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,33 @@
# Basic data types for Migrate.
migrate_plugin:
type: mapping
mapping:
plugin:
type: string
label: 'Plugin'
migrate_destination:
type: migrate_plugin
label: 'Destination'
migrate_source:
type: migrate_plugin
label: 'Source'
mapping:
constants:
type: ignore
label: 'Constants'
# Base schema for migrate source plugins that extend
# \Drupal\migrate\Plugin\migrate\source\SqlBase.
migrate_source_sql:
type: migrate_source
mapping:
target:
type: string
label: 'The migration database target'
migrate_load:
type: migrate_plugin
label: 'Load'

View file

@ -0,0 +1,34 @@
# Schema for the migrate destination plugins.
migrate.destination.*:
type: migrate_destination
label: 'Default destination'
mapping:
no_stub:
type: boolean
label: 'Whether stubbing is allowed.'
default: false
migrate.destination.config:
type: migrate_destination
label: 'Config'
mapping:
config_name:
type: string
label: 'Configuration name'
migrate.destination.entity:user:
type: migrate_destination
label: 'User'
mapping:
md5_passwords:
type: boolean
label: 'Passwords'
migrate.destination.entity:file:
type: migrate_destination
label: 'Picture'
mapping:
source_path_property:
type: string
label: 'Source path'

View file

@ -0,0 +1,21 @@
# Schema for the migrate load plugins.
migrate.load.*:
type: migrate_load
label: 'Default load'
migrate.load.drupal_entity:
type: migrate_load
label: 'Default source'
mapping:
bundle_migration:
type: string
label: 'Bundle migration'
migrate.load.d6_term_node:
type: migrate_load
label: 'Default source'
mapping:
bundle_migration:
type: string
label: 'Bundle migration'

View file

@ -0,0 +1,46 @@
# Schema for the configuration files of the Migrate module.
migrate.migration.*:
type: config_entity
label: 'Migration'
mapping:
id:
type: string
label: 'ID'
migration_tags:
type: sequence
label: 'Migration Tags'
sequence:
type: string
label: 'Tag'
label:
type: label
label: 'Label'
load:
type: migrate.load.[plugin]
label: 'Source'
source:
type: migrate.source.[plugin]
label: 'Source'
process:
type: ignore
label: 'Process'
destination:
type: migrate.destination.[plugin]
label: 'Destination'
migration_dependencies:
type: mapping
label: 'Dependencies'
mapping:
required:
type: sequence
label: 'Required dependencies'
sequence:
type: string
label: 'Dependency'
optional:
type: sequence
label: 'Optional dependencies'
sequence:
type: string
label: 'Dependency'

View file

@ -0,0 +1,13 @@
# Schema for the migrate source plugins.
migrate.source.*:
type: migrate_source
label: 'Default source'
migrate.source.empty:
type: migrate_source_sql
label: 'Empty source'
mapping:
provider:
type: string
label: 'Provider'

View file

@ -0,0 +1,131 @@
<?php
/**
* @file
* Hooks provided by the Migrate module.
*/
use Drupal\migrate\Entity\MigrationInterface;
use Drupal\migrate\Plugin\MigrateSourceInterface;
use Drupal\migrate\Row;
/**
* @defgroup migration Migration API
* @{
* Overview of the Migration API, which migrates data into Drupal.
*
* @section overview Overview of migration
* Migration is an
* @link http://wikipedia.org/wiki/Extract,_transform,_load Extract, Transform, Load @endlink
* (ETL) process. For historical reasons, in the Drupal migration tool the
* extract phase is called "source", the transform phase is called "process",
* and the load phase is called "destination".
*
* Source, process, and destination phases are each provided by plugins. Source
* plugins extract data from a data source in "rows", containing "properties".
* Each row is handed off to one or more series of process plugins, where each
* series operates to transform the row data into one result property. After all
* the properties are processed, the resulting row is handed off to a
* destination plugin, which saves the data.
*
* The Migrate module provides process plugins for common operations (setting
* default values, mapping values, etc.), and destination plugins for Drupal
* core objects (configuration, entity, URL alias, etc.). The Migrate Drupal
* module provides source plugins to extract data from various versions of
* Drupal. Custom and contributed modules can provide additional plugins; see
* the @link plugin_api Plugin API topic @endlink for generic information about
* providing plugins, and sections below for details about the plugin types.
*
* The configuration of migrations is stored in configuration entities, which
* list the IDs and configurations of the plugins that are involved. See
* @ref sec_entity below for details. To migrate an entire site, you'll need to
* create a migration manifest; see @ref sec_manifest for details.
*
* https://www.drupal.org/node/2127611 has more complete information on the
* Migration API, including information on load plugins, which are only used
* in Drupal 6 migration.
*
* @section sec_source Source plugins
* Migration source plugins implement
* \Drupal\migrate\Plugin\MigrateSourceInterface and usually extend
* \Drupal\migrate\Plugin\migrate\source\SourcePluginBase. They are annotated
* with \Drupal\migrate\Annotation\MigrateSource annotation, and must be in
* namespace subdirectory Plugin\migrate\source under the namespace of the
* module that defines them. Migration source plugins are managed by the
* \Drupal\migrate\Plugin\MigratePluginManager class.
*
* @section sec_process Process plugins
* Migration process plugins implement
* \Drupal\migrate\Plugin\MigrateProcessInterface and usually extend
* \Drupal\migrate\ProcessPluginBase. They are annotated
* with \Drupal\migrate\Annotation\MigrateProcessPlugin annotation, and must be
* in namespace subdirectory Plugin\migrate\process under the namespace of the
* module that defines them. Migration process plugins are managed by the
* \Drupal\migrate\Plugin\MigratePluginManager class.
*
* @section sec_destination Destination plugins
* Migration destination plugins implement
* \Drupal\migrate\Plugin\MigrateDestinationInterface and usually extend
* \Drupal\migrate\Plugin\migrate\destination\DestinationBase. They are
* annotated with \Drupal\migrate\Annotation\MigrateDestination annotation, and
* must be in namespace subdirectory Plugin\migrate\destination under the
* namespace of the module that defines them. Migration destination plugins
* are managed by the
* \Drupal\migrate\Plugin\MigrateDestinationPluginManager class.
*
* @section sec_entity Migration configuration entities
* The definition of how to migrate each type of data is stored in configuration
* entities. The migration configuration entity class is
* \Drupal\migrate\Entity\Migration, with interface
* \Drupal\migrate\Entity\MigrationInterface; the configuration schema can be
* found in the migrate.schema.yml file. Migration configuration consists of IDs
* and configuration for the source, process, and destination plugins, as well
* as information on dependencies. Process configuration consists of sections,
* each of which defines the series of process plugins needed for one
* destination property. You can find examples of migration configuration files
* in the core/modules/migrate_drupal/config/install directory.
*
* @section sec_manifest Migration manifests
* You can run a migration with the "drush migrate-manifest" command, providing
* a migration manifest file. This file lists the configuration names of the
* migrations you want to execute, as well as any dependencies they have (you
* can find these in the "migration_dependencies" sections of the individual
* configuration files). For example, to migrate blocks from a Drupal 6 site,
* you would list:
* @code
* # Migrate blocks from Drupal 6 to 8
* - d6_filter_format
* - d6_custom_block
* - d6_block
* @endcode
* @}
*/
/**
* @addtogroup hooks
* @{
*/
/**
* Allows adding data to a row before processing it.
*
* For example, filter module used to store filter format settings in the
* variables table which now needs to be inside the filter format config
* file. So, it needs to be added here.
*
* hook_migrate_MIGRATION_ID_prepare_row() is also available.
*
* @ingroup migration
*/
function hook_migrate_prepare_row(Row $row, MigrateSourceInterface $source, MigrationInterface $migration) {
if ($migration->id() == 'd6_filter_formats') {
$value = $source->getDatabase()->query('SELECT value FROM {variable} WHERE name = :name', array(':name' => 'mymodule_filter_foo_' . $row->getSourceProperty('format')))->fetchField();
if ($value) {
$row->setSourceProperty('settings:mymodule:foo', unserialize($value));
}
}
}
/**
* @} End of "addtogroup hooks".
*/

View file

@ -0,0 +1,7 @@
name: Migrate
type: module
description: 'Handles migrations'
package: Core
version: VERSION
core: 8.x
;configure: admin/structure/migrate

View file

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

View file

@ -0,0 +1,22 @@
services:
cache.migrate:
class: Drupal\Core\Cache\CacheBackendInterface
tags:
- { name: cache.bin }
factory: cache_factory:get
arguments: [migrate]
plugin.manager.migrate.source:
class: Drupal\migrate\Plugin\MigratePluginManager
arguments: [source, '@container.namespaces', '@cache.discovery', '@module_handler', 'Drupal\migrate\Annotation\MigrateSource']
plugin.manager.migrate.process:
class: Drupal\migrate\Plugin\MigratePluginManager
arguments: [process, '@container.namespaces', '@cache.discovery', '@module_handler', 'Drupal\migrate\Annotation\MigrateProcessPlugin']
plugin.manager.migrate.destination:
class: Drupal\migrate\Plugin\MigrateDestinationPluginManager
arguments: [destination, '@container.namespaces', '@cache.discovery', '@module_handler', '@entity.manager']
plugin.manager.migrate.id_map:
class: Drupal\migrate\Plugin\MigratePluginManager
arguments: [id_map, '@container.namespaces', '@cache.discovery', '@module_handler']
password_migrate:
class: Drupal\migrate\MigratePassword
arguments: ['@password_original']

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() {}
}

View file

@ -0,0 +1,31 @@
<?php
/**
* @file
* Contains \Drupal\Tests\migrate\Unit\Entity\MigrationTest.
*/
namespace Drupal\Tests\migrate\Unit\Entity;
use Drupal\migrate\Entity\Migration;
use Drupal\Tests\UnitTestCase;
/**
* Tests the migrate entity.
*
* @coversDefaultClass \Drupal\migrate\Entity\Migration
* @group migrate
*/
class MigrationTest extends UnitTestCase {
/**
* Tests Migration::getProcessPlugins()
*
* @covers ::getProcessPlugins
*/
public function testGetProcessPlugins() {
$migration = new Migration([], 'migration');
$this->assertEquals([], $migration->getProcessPlugins([]));
}
}

View file

@ -0,0 +1,120 @@
<?php
/**
* @file
* Contains \Drupal\Tests\migrate\Unit\MigrateExecutableMemoryExceededTest.
*/
namespace Drupal\Tests\migrate\Unit;
/**
* Tests the \Drupal\migrate\MigrateExecutable::memoryExceeded() method.
*
* @group migrate
*/
class MigrateExecutableMemoryExceededTest extends MigrateTestCase {
/**
* The mocked migration entity.
*
* @var \Drupal\migrate\Entity\MigrationInterface|\PHPUnit_Framework_MockObject_MockObject
*/
protected $migration;
/**
* The mocked migrate message.
*
* @var \Drupal\migrate\MigrateMessageInterface|\PHPUnit_Framework_MockObject_MockObject
*/
protected $message;
/**
* The tested migrate executable.
*
* @var \Drupal\Tests\migrate\Unit\TestMigrateExecutable
*/
protected $executable;
/**
* The migration configuration, initialized to set the ID to test.
*
* @var array
*/
protected $migrationConfiguration = array(
'id' => 'test',
);
/**
* php.init memory_limit value.
*/
protected $memoryLimit = 10000000;
/**
* {@inheritdoc}
*/
protected function setUp() {
parent::setUp();
$this->migration = $this->getMigration();
$this->message = $this->getMock('Drupal\migrate\MigrateMessageInterface');
$this->executable = new TestMigrateExecutable($this->migration, $this->message);
$this->executable->setStringTranslation($this->getStringTranslationStub());
}
/**
* Runs the actual test.
*
* @param string $message
* The second message to assert.
* @param bool $memory_exceeded
* Whether to test the memory exceeded case.
* @param int $memory_usage_first
* (optional) The first memory usage value.
* @param int $memory_usage_second
* (optional) The fake amount of memory usage reported after memory reclaim.
* @param int $memory_limit
* (optional) The memory limit.
*/
protected function runMemoryExceededTest($message, $memory_exceeded, $memory_usage_first = NULL, $memory_usage_second = NULL, $memory_limit = NULL) {
$this->executable->setMemoryLimit($memory_limit ?: $this->memoryLimit);
$this->executable->setMemoryUsage($memory_usage_first ?: $this->memoryLimit, $memory_usage_second ?: $this->memoryLimit);
$this->executable->setMemoryThreshold(0.85);
if ($message) {
$this->executable->message->expects($this->at(0))
->method('display')
->with($this->stringContains('reclaiming memory'));
$this->executable->message->expects($this->at(1))
->method('display')
->with($this->stringContains($message));
}
else {
$this->executable->message->expects($this->never())
->method($this->anything());
}
$result = $this->executable->memoryExceeded();
$this->assertEquals($memory_exceeded, $result);
}
/**
* Tests memoryExceeded method when a new batch is needed.
*/
public function testMemoryExceededNewBatch() {
// First case try reset and then start new batch.
$this->runMemoryExceededTest('starting new batch', TRUE);
}
/**
* Tests memoryExceeded method when enough is cleared.
*/
public function testMemoryExceededClearedEnough() {
$this->runMemoryExceededTest('reclaimed enough', FALSE, $this->memoryLimit, $this->memoryLimit * 0.75);
}
/**
* Tests memoryExceeded when memory usage is not exceeded.
*/
public function testMemoryNotExceeded() {
$this->runMemoryExceededTest('', FALSE, floor($this->memoryLimit * 0.85) - 1);
}
}

View file

@ -0,0 +1,525 @@
<?php
/**
* @file
* Contains \Drupal\Tests\migrate\Unit\MigrateExecutableTest.
*/
namespace Drupal\Tests\migrate\Unit;
use Drupal\migrate\Entity\MigrationInterface;
use Drupal\migrate\Plugin\MigrateIdMapInterface;
use Drupal\migrate\MigrateException;
use Drupal\migrate\Row;
/**
* @coversDefaultClass \Drupal\Tests\migrate\Unit\MigrateExecutableTest
* @group migrate
*/
class MigrateExecutableTest extends MigrateTestCase {
/**
* The mocked migration entity.
*
* @var \Drupal\migrate\Entity\MigrationInterface|\PHPUnit_Framework_MockObject_MockObject
*/
protected $migration;
/**
* The mocked migrate message.
*
* @var \Drupal\migrate\MigrateMessageInterface|\PHPUnit_Framework_MockObject_MockObject
*/
protected $message;
/**
* The tested migrate executable.
*
* @var \Drupal\Tests\migrate\Unit\TestMigrateExecutable
*/
protected $executable;
protected $migrationConfiguration = array(
'id' => 'test',
);
/**
* {@inheritdoc}
*/
protected function setUp() {
parent::setUp();
$this->migration = $this->getMigration();
$this->message = $this->getMock('Drupal\migrate\MigrateMessageInterface');
$this->executable = new TestMigrateExecutable($this->migration, $this->message);
$this->executable->setStringTranslation($this->getStringTranslationStub());
$this->executable->setTimeThreshold(0.1);
$this->executable->limit = array('unit' => 'second', 'value' => 1);
}
/**
* Tests an import with an incomplete rewinding.
*/
public function testImportWithFailingRewind() {
$exception_message = $this->getRandomGenerator()->string();
$source = $this->getMock('Drupal\migrate\Plugin\MigrateSourceInterface');
$source->expects($this->once())
->method('rewind')
->will($this->throwException(new \Exception($exception_message)));
$this->migration->expects($this->any())
->method('getSourcePlugin')
->will($this->returnValue($source));
// Ensure that a message with the proper message was added.
$this->message->expects($this->once())
->method('display')
->with("Migration failed with source plugin exception: $exception_message");
$result = $this->executable->import();
$this->assertEquals(MigrationInterface::RESULT_FAILED, $result);
}
/**
* Tests the import method with a valid row.
*/
public function testImportWithValidRow() {
$source = $this->getMockSource();
$row = $this->getMockBuilder('Drupal\migrate\Row')
->disableOriginalConstructor()
->getMock();
$row->expects($this->once())
->method('getSourceIdValues')
->will($this->returnValue(array('id' => 'test')));
$this->idMap->expects($this->once())
->method('lookupDestinationId')
->with(array('id' => 'test'))
->will($this->returnValue(array('test')));
$source->expects($this->once())
->method('current')
->will($this->returnValue($row));
$this->executable->setSource($source);
$this->migration->expects($this->once())
->method('getProcessPlugins')
->will($this->returnValue(array()));
$destination = $this->getMock('Drupal\migrate\Plugin\MigrateDestinationInterface');
$destination->expects($this->once())
->method('import')
->with($row, array('test'))
->will($this->returnValue(array('id' => 'test')));
$this->migration->expects($this->once())
->method('getDestinationPlugin')
->will($this->returnValue($destination));
$this->assertSame(MigrationInterface::RESULT_COMPLETED, $this->executable->import());
$this->assertSame(1, $this->executable->getSuccessesSinceFeedback());
$this->assertSame(1, $this->executable->getTotalSuccesses());
$this->assertSame(1, $this->executable->getTotalProcessed());
$this->assertSame(1, $this->executable->getProcessedSinceFeedback());
}
/**
* Tests the import method with a valid row.
*/
public function testImportWithValidRowWithoutDestinationId() {
$source = $this->getMockSource();
$row = $this->getMockBuilder('Drupal\migrate\Row')
->disableOriginalConstructor()
->getMock();
$row->expects($this->once())
->method('getSourceIdValues')
->will($this->returnValue(array('id' => 'test')));
$this->idMap->expects($this->once())
->method('lookupDestinationId')
->with(array('id' => 'test'))
->will($this->returnValue(array('test')));
$source->expects($this->once())
->method('current')
->will($this->returnValue($row));
$this->executable->setSource($source);
$this->migration->expects($this->once())
->method('getProcessPlugins')
->will($this->returnValue(array()));
$destination = $this->getMock('Drupal\migrate\Plugin\MigrateDestinationInterface');
$destination->expects($this->once())
->method('import')
->with($row, array('test'))
->will($this->returnValue(TRUE));
$this->migration->expects($this->once())
->method('getDestinationPlugin')
->will($this->returnValue($destination));
$this->idMap->expects($this->never())
->method('saveIdMapping');
$this->assertSame(MigrationInterface::RESULT_COMPLETED, $this->executable->import());
$this->assertSame(1, $this->executable->getSuccessesSinceFeedback());
$this->assertSame(1, $this->executable->getTotalSuccesses());
$this->assertSame(1, $this->executable->getTotalProcessed());
$this->assertSame(1, $this->executable->getProcessedSinceFeedback());
}
/**
* Tests the import method with a valid row.
*/
public function testImportWithValidRowNoDestinationValues() {
$source = $this->getMockSource();
$row = $this->getMockBuilder('Drupal\migrate\Row')
->disableOriginalConstructor()
->getMock();
$row->expects($this->once())
->method('getSourceIdValues')
->will($this->returnValue(array('id' => 'test')));
$source->expects($this->once())
->method('current')
->will($this->returnValue($row));
$this->executable->setSource($source);
$this->migration->expects($this->once())
->method('getProcessPlugins')
->will($this->returnValue(array()));
$destination = $this->getMock('Drupal\migrate\Plugin\MigrateDestinationInterface');
$destination->expects($this->once())
->method('import')
->with($row, array('test'))
->will($this->returnValue(array()));
$this->migration->expects($this->once())
->method('getDestinationPlugin')
->will($this->returnValue($destination));
$this->idMap->expects($this->once())
->method('delete')
->with(array('id' => 'test'), TRUE);
$this->idMap->expects($this->once())
->method('saveIdMapping')
->with($row, array(), MigrateIdMapInterface::STATUS_FAILED, NULL);
$this->idMap->expects($this->once())
->method('messageCount')
->will($this->returnValue(0));
$this->idMap->expects($this->once())
->method('saveMessage');
$this->idMap->expects($this->once())
->method('lookupDestinationId')
->with(array('id' => 'test'))
->will($this->returnValue(array('test')));
$this->message->expects($this->once())
->method('display')
->with('New object was not saved, no error provided');
$this->assertSame(MigrationInterface::RESULT_COMPLETED, $this->executable->import());
}
/**
* Tests the import method with a MigrateException being thrown.
*/
public function testImportWithValidRowWithMigrateException() {
$exception_message = $this->getRandomGenerator()->string();
$source = $this->getMockSource();
$row = $this->getMockBuilder('Drupal\migrate\Row')
->disableOriginalConstructor()
->getMock();
$row->expects($this->once())
->method('getSourceIdValues')
->will($this->returnValue(array('id' => 'test')));
$source->expects($this->once())
->method('current')
->will($this->returnValue($row));
$this->executable->setSource($source);
$this->migration->expects($this->once())
->method('getProcessPlugins')
->will($this->returnValue(array()));
$destination = $this->getMock('Drupal\migrate\Plugin\MigrateDestinationInterface');
$destination->expects($this->once())
->method('import')
->with($row, array('test'))
->will($this->throwException(new MigrateException($exception_message)));
$this->migration->expects($this->once())
->method('getDestinationPlugin')
->will($this->returnValue($destination));
$this->idMap->expects($this->once())
->method('saveIdMapping')
->with($row, array(), MigrateIdMapInterface::STATUS_FAILED, NULL);
$this->idMap->expects($this->once())
->method('saveMessage');
$this->message->expects($this->once())
->method('display')
->with($exception_message);
$this->idMap->expects($this->once())
->method('lookupDestinationId')
->with(array('id' => 'test'))
->will($this->returnValue(array('test')));
$this->assertSame(MigrationInterface::RESULT_COMPLETED, $this->executable->import());
}
/**
* Tests the import method with a regular Exception being thrown.
*/
public function testImportWithValidRowWithException() {
$exception_message = $this->getRandomGenerator()->string();
$source = $this->getMockSource();
$row = $this->getMockBuilder('Drupal\migrate\Row')
->disableOriginalConstructor()
->getMock();
$row->expects($this->once())
->method('getSourceIdValues')
->will($this->returnValue(array('id' => 'test')));
$source->expects($this->once())
->method('current')
->will($this->returnValue($row));
$this->executable->setSource($source);
$this->migration->expects($this->once())
->method('getProcessPlugins')
->will($this->returnValue(array()));
$destination = $this->getMock('Drupal\migrate\Plugin\MigrateDestinationInterface');
$destination->expects($this->once())
->method('import')
->with($row, array('test'))
->will($this->throwException(new \Exception($exception_message)));
$this->migration->expects($this->once())
->method('getDestinationPlugin')
->will($this->returnValue($destination));
$this->idMap->expects($this->once())
->method('saveIdMapping')
->with($row, array(), MigrateIdMapInterface::STATUS_FAILED, NULL);
$this->idMap->expects($this->once())
->method('saveMessage');
$this->idMap->expects($this->once())
->method('lookupDestinationId')
->with(array('id' => 'test'))
->will($this->returnValue(array('test')));
$this->message->expects($this->once())
->method('display')
->with($exception_message);
$this->assertSame(MigrationInterface::RESULT_COMPLETED, $this->executable->import());
}
/**
* Tests time limit option method.
*/
public function testTimeOptionExceeded() {
// Assert time limit of one second (test configuration default) is exceeded.
$this->executable->setTimeElapsed(1);
$this->assertTrue($this->executable->timeOptionExceeded());
// Assert time limit not exceeded.
$this->executable->limit = array('unit' => 'seconds', 'value' => (int) $_SERVER['REQUEST_TIME'] - 3600);
$this->assertFalse($this->executable->timeOptionExceeded());
// Assert no time limit.
$this->executable->limit = array();
$this->assertFalse($this->executable->timeOptionExceeded());
}
/**
* Tests get time limit method.
*/
public function testGetTimeLimit() {
// Assert time limit has a unit of one second (test configuration default).
$limit = $this->executable->limit;
$this->assertArrayHasKey('unit', $limit);
$this->assertSame('second', $limit['unit']);
$this->assertSame($limit['value'], $this->executable->getTimeLimit());
// Assert time limit has a unit of multiple seconds.
$this->executable->limit = array('unit' => 'seconds', 'value' => 30);
$limit = $this->executable->limit;
$this->assertArrayHasKey('unit', $limit);
$this->assertSame('seconds', $limit['unit']);
$this->assertSame($limit['value'], $this->executable->getTimeLimit());
// Assert no time limit.
$this->executable->limit = array();
$limit = $this->executable->limit;
$this->assertArrayNotHasKey('unit', $limit);
$this->assertArrayNotHasKey('value', $limit);
$this->assertNull($this->executable->getTimeLimit());
}
/**
* Tests saving of queued messages.
*/
public function testSaveQueuedMessages() {
// Assert no queued messages before save.
$this->assertAttributeEquals(array(), 'queuedMessages', $this->executable);
// Set required source_id_values for MigrateIdMapInterface::saveMessage().
$expected_messages[] = array('message' => 'message 1', 'level' => MigrationInterface::MESSAGE_ERROR);
$expected_messages[] = array('message' => 'message 2', 'level' => MigrationInterface::MESSAGE_WARNING);
$expected_messages[] = array('message' => 'message 3', 'level' => MigrationInterface::MESSAGE_INFORMATIONAL);
foreach ($expected_messages as $queued_message) {
$this->executable->queueMessage($queued_message['message'], $queued_message['level']);
}
$this->executable->setSourceIdValues(array());
$this->assertAttributeEquals($expected_messages, 'queuedMessages', $this->executable);
// No asserts of saved messages since coverage exists
// in MigrateSqlIdMapTest::saveMessage().
$this->executable->saveQueuedMessages();
// Assert no queued messages after save.
$this->assertAttributeEquals(array(), 'queuedMessages', $this->executable);
}
/**
* Tests the queuing of messages.
*/
public function testQueueMessage() {
// Assert no queued messages.
$expected_messages = array();
$this->assertAttributeEquals(array(), 'queuedMessages', $this->executable);
// Assert a single (default level) queued message.
$expected_messages[] = array(
'message' => 'message 1',
'level' => MigrationInterface::MESSAGE_ERROR,
);
$this->executable->queueMessage('message 1');
$this->assertAttributeEquals($expected_messages, 'queuedMessages', $this->executable);
// Assert multiple queued messages.
$expected_messages[] = array(
'message' => 'message 2',
'level' => MigrationInterface::MESSAGE_WARNING,
);
$this->executable->queueMessage('message 2', MigrationInterface::MESSAGE_WARNING);
$this->assertAttributeEquals($expected_messages, 'queuedMessages', $this->executable);
$expected_messages[] = array(
'message' => 'message 3',
'level' => MigrationInterface::MESSAGE_INFORMATIONAL,
);
$this->executable->queueMessage('message 3', MigrationInterface::MESSAGE_INFORMATIONAL);
$this->assertAttributeEquals($expected_messages, 'queuedMessages', $this->executable);
}
/**
* Tests maximum execution time (max_execution_time) of an import.
*/
public function testMaxExecTimeExceeded() {
// Assert no max_execution_time value.
$this->executable->setMaxExecTime(0);
$this->assertFalse($this->executable->maxExecTimeExceeded());
// Assert default max_execution_time value does not exceed.
$this->executable->setMaxExecTime(30);
$this->assertFalse($this->executable->maxExecTimeExceeded());
// Assert max_execution_time value is exceeded.
$this->executable->setMaxExecTime(1);
$this->executable->setTimeElapsed(2);
$this->assertTrue($this->executable->maxExecTimeExceeded());
}
/**
* Tests the processRow method.
*/
public function testProcessRow() {
$expected = array(
'test' => 'test destination',
'test1' => 'test1 destination'
);
foreach ($expected as $key => $value) {
$plugins[$key][0] = $this->getMock('Drupal\migrate\Plugin\MigrateProcessInterface');
$plugins[$key][0]->expects($this->once())
->method('getPluginDefinition')
->will($this->returnValue(array()));
$plugins[$key][0]->expects($this->once())
->method('transform')
->will($this->returnValue($value));
}
$this->migration->expects($this->once())
->method('getProcessPlugins')
->with(NULL)
->will($this->returnValue($plugins));
$row = new Row(array(), array());
$this->executable->processRow($row);
foreach ($expected as $key => $value) {
$this->assertSame($row->getDestinationProperty($key), $value);
}
$this->assertSame(count($row->getDestination()), count($expected));
}
/**
* Tests the processRow method with an empty pipeline.
*/
public function testProcessRowEmptyPipeline() {
$this->migration->expects($this->once())
->method('getProcessPlugins')
->with(NULL)
->will($this->returnValue(array('test' => array())));
$row = new Row(array(), array());
$this->executable->processRow($row);
$this->assertSame($row->getDestination(), array());
}
/**
* Returns a mock migration source instance.
*
* @return \Drupal\migrate\Plugin\MigrateSourceInterface|\PHPUnit_Framework_MockObject_MockObject
*/
protected function getMockSource() {
$iterator = $this->getMock('\Iterator');
$class = 'Drupal\migrate\Plugin\migrate\source\SourcePluginBase';
$source = $this->getMockBuilder($class)
->disableOriginalConstructor()
->setMethods(get_class_methods($class))
->getMockForAbstractClass();
$source->expects($this->any())
->method('getIterator')
->will($this->returnValue($iterator));
$source->expects($this->once())
->method('rewind')
->will($this->returnValue(TRUE));
$source->expects($this->any())
->method('initializeIterator')
->will($this->returnValue([]));
$source->expects($this->any())
->method('valid')
->will($this->onConsecutiveCalls(TRUE, FALSE));
return $source;
}
}

View file

@ -0,0 +1,245 @@
<?php
/**
* @file
* Contains \Drupal\Tests\migrate\Unit\MigrateSourceTest.
*/
namespace Drupal\Tests\migrate\Unit;
use Drupal\Core\DependencyInjection\ContainerBuilder;
use Drupal\migrate\MigrateExecutable;
use Drupal\migrate\Plugin\MigrateIdMapInterface;
/**
* @coversDefaultClass \Drupal\migrate\Plugin\migrate\source\SourcePluginBase
* @group migrate
*/
class MigrateSourceTest extends MigrateTestCase {
/**
* Override the migration config.
*
* @var array
*/
protected $defaultMigrationConfiguration = [
'id' => 'test_migration',
'source' => [],
];
/**
* Test row data.
*
* @var array
*/
protected $row = ['test_sourceid1' => '1', 'timestamp' => 500];
/**
* Test source ids.
*
* @var array
*/
protected $sourceIds = ['test_sourceid1' => 'test_sourceid1'];
/**
* The migration entity.
*
* @var \Drupal\migrate\Entity\Migration
*/
protected $migration;
/**
* The migrate executable.
*
* @var \Drupal\migrate\MigrateExecutable
*/
protected $executable;
/**
* Get the source plugin to test.
*
* @param array $configuration
* The source configuration.
* @param array $migrate_config
* The migration configuration to be used in parent::getMigration().
* @param int $status
* The default status for the new rows to be imported.
*
* @return \Drupal\migrate\Plugin\MigrateSourceInterface
* A mocked source plugin.
*/
protected function getSource($configuration = [], $migrate_config = [], $status = MigrateIdMapInterface::STATUS_NEEDS_UPDATE) {
$this->migrationConfiguration = $this->defaultMigrationConfiguration + $migrate_config;
$this->migration = parent::getMigration();
$this->executable = $this->getMigrateExecutable($this->migration);
// Update the idMap for Source so the default is that the row has already
// been imported. This allows us to use the highwater mark to decide on the
// outcome of whether we choose to import the row.
$id_map_array = ['original_hash' => '', 'hash' => '', 'source_row_status' => $status];
$this->idMap
->expects($this->any())
->method('getRowBySource')
->willReturn($id_map_array);
$constructor_args = [$configuration, 'd6_action', [], $this->migration];
$methods = ['getModuleHandler', 'fields', 'getIds', '__toString', 'getIterator', 'prepareRow', 'initializeIterator', 'calculateDependencies'];
$source_plugin = $this->getMock('\Drupal\migrate\Plugin\migrate\source\SourcePluginBase', $methods, $constructor_args);
$source_plugin
->expects($this->any())
->method('fields')
->willReturn([]);
$source_plugin
->expects($this->any())
->method('getIds')
->willReturn([]);
$source_plugin
->expects($this->any())
->method('__toString')
->willReturn('');
$source_plugin
->expects($this->any())
->method('prepareRow')
->willReturn(empty($migrate_config['prepare_row_false']));
$source_plugin
->expects($this->any())
->method('initializeIterator')
->willReturn([]);
$iterator = new \ArrayIterator([$this->row]);
$source_plugin
->expects($this->any())
->method('getIterator')
->willReturn($iterator);
$module_handler = $this->getMock('\Drupal\Core\Extension\ModuleHandlerInterface');
$source_plugin
->expects($this->any())
->method('getModuleHandler')
->willReturn($module_handler);
$this->migration
->expects($this->any())
->method('getSourcePlugin')
->willReturn($source_plugin);
return $this->migration->getSourcePlugin();
}
/**
* @expectedException \Drupal\migrate\MigrateException
*/
public function testHighwaterTrackChangesIncompatible() {
$source_config = ['track_changes' => TRUE];
$migration_config = ['highWaterProperty' => ['name' => 'something']];
$this->getSource($source_config, $migration_config);
}
/**
* Test that the source count is correct.
*/
public function testCount() {
$container = new ContainerBuilder();
$container->register('cache.migrate', 'Drupal\Core\Cache\NullBackend')
->setArguments(['migrate']);
\Drupal::setContainer($container);
// Test that the basic count works.
$source = $this->getSource();
$this->assertEquals(1, $source->count());
// Test caching the count works.
$source = $this->getSource(['cache_counts' => TRUE]);
$this->assertEquals(1, $source->count());
// Test the skip argument.
$source = $this->getSource(['skip_count' => TRUE]);
$this->assertEquals(-1, $source->count());
}
/**
* Test that we don't get a row if prepareRow() is false.
*/
public function testPrepareRowFalse() {
$source = $this->getSource([], ['prepare_row_false' => TRUE]);
$source->rewind();
$this->assertNull($source->current(), 'No row is available when prepareRow() is false.');
}
/**
* Test that the when a source id is in the idList, we don't get a row.
*/
public function testIdInList() {
$source = $this->getSource([], ['idlist' => ['test_sourceid1']]);
$source->rewind();
$this->assertNull($source->current(), 'No row is available because id was in idList.');
}
/**
* Test that $row->needsUpdate() works as expected.
*/
public function testNextNeedsUpdate() {
$source = $this->getSource();
// $row->needsUpdate() === TRUE so we get a row.
$source->rewind();
$this->assertTrue(is_a($source->current(), 'Drupal\migrate\Row'), '$row->needsUpdate() is TRUE so we got a row.');
// Test that we don't get a row when the incoming row is marked as imported.
$source = $this->getSource([], [], MigrateIdMapInterface::STATUS_IMPORTED);
$source->rewind();
$this->assertNull($source->current(), 'Row was already imported, should be NULL');
}
/**
* Test that an outdated highwater mark does not cause a row to be imported.
*/
public function testOutdatedHighwater() {
$source = $this->getSource([], [], MigrateIdMapInterface::STATUS_IMPORTED);
// Set the originalHighwater to something higher than our timestamp.
$this->migration
->expects($this->any())
->method('getHighwater')
->willReturn($this->row['timestamp'] + 1);
// The current highwater mark is now higher than the row timestamp so no row
// is expected.
$source->rewind();
$this->assertNull($source->current(), 'Original highwater mark is higher than incoming row timestamp.');
}
/**
* Test that a highwater mark newer than our saved one imports a row.
*
* @throws \Exception
*/
public function testNewHighwater() {
// Set a highwater property field for source. Now we should have a row
// because the row timestamp is greater than the current highwater mark.
$source = $this->getSource([], ['highWaterProperty' => ['name' => 'timestamp']], MigrateIdMapInterface::STATUS_IMPORTED);
$source->rewind();
$this->assertTrue(is_a($source->current(), 'Drupal\migrate\Row'), 'Incoming row timestamp is greater than current highwater mark so we have a row.');
}
/**
* Get a mock executable for the test.
*
* @param \Drupal\migrate\Entity\MigrationInterface $migration
* The migration entity.
*
* @return \Drupal\migrate\MigrateExecutable
* The migrate executable.
*/
protected function getMigrateExecutable($migration) {
$message = $this->getMock('Drupal\migrate\MigrateMessageInterface');
return new MigrateExecutable($migration, $message);
}
}

View file

@ -0,0 +1,212 @@
<?php
/**
* @file
* Contains \Drupal\Tests\migrate\Unit\MigrateSqlIdMapEnsureTablesTest.
*/
namespace Drupal\Tests\migrate\Unit;
use Drupal\migrate\Plugin\MigrateIdMapInterface;
/**
* Tests the SQL ID map plugin ensureTables() method.
*
* @group migrate
*/
class MigrateSqlIdMapEnsureTablesTest extends MigrateTestCase {
/**
* The migration configuration, initialized to set the ID and destination IDs.
*
* @var array
*/
protected $migrationConfiguration = array(
'id' => 'sql_idmap_test',
);
/**
* Tests the ensureTables method when the tables do not exist.
*/
public function testEnsureTablesNotExist() {
$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',
);
$fields['sourceid1'] = array(
'type' => 'int',
'not null' => TRUE,
);
$fields['destid1'] = array(
'type' => 'varchar',
'length' => 255,
'not null' => FALSE,
);
$map_table_schema = array(
'description' => 'Mappings from source identifier value(s) to destination identifier value(s).',
'fields' => $fields,
'primary key' => array('sourceid1'),
);
$schema = $this->getMockBuilder('Drupal\Core\Database\Schema')
->disableOriginalConstructor()
->getMock();
$schema->expects($this->at(0))
->method('tableExists')
->with('migrate_map_sql_idmap_test')
->will($this->returnValue(FALSE));
$schema->expects($this->at(1))
->method('createTable')
->with('migrate_map_sql_idmap_test', $map_table_schema);
// Now do the message table.
$fields = array();
$fields['msgid'] = array(
'type' => 'serial',
'unsigned' => TRUE,
'not null' => TRUE,
);
$fields['sourceid1'] = array(
'type' => 'int',
'not null' => TRUE,
);
$fields['level'] = array(
'type' => 'int',
'unsigned' => TRUE,
'not null' => TRUE,
'default' => 1,
);
$fields['message'] = array(
'type' => 'text',
'size' => 'medium',
'not null' => TRUE,
);
$table_schema = array(
'description' => 'Messages generated during a migration process',
'fields' => $fields,
'primary key' => array('msgid'),
);
$table_schema['indexes']['sourcekey'] = array('sourceid1');
$schema->expects($this->at(2))
->method('tableExists')
->with('migrate_message_sql_idmap_test')
->will($this->returnValue(FALSE));
$schema->expects($this->at(3))
->method('createTable')
->with('migrate_message_sql_idmap_test', $table_schema);
$schema->expects($this->any())
->method($this->anything());
$this->runEnsureTablesTest($schema);
}
/**
* Tests the ensureTables method when the tables exist.
*/
public function testEnsureTablesExist() {
$schema = $this->getMockBuilder('Drupal\Core\Database\Schema')
->disableOriginalConstructor()
->getMock();
$schema->expects($this->at(0))
->method('tableExists')
->with('migrate_map_sql_idmap_test')
->will($this->returnValue(TRUE));
$schema->expects($this->at(1))
->method('fieldExists')
->with('migrate_map_sql_idmap_test', 'rollback_action')
->will($this->returnValue(FALSE));
$field_schema = array(
'type' => 'int',
'size' => 'tiny',
'unsigned' => TRUE,
'not null' => TRUE,
'default' => 0,
'description' => 'Flag indicating what to do for this item on rollback',
);
$schema->expects($this->at(2))
->method('addField')
->with('migrate_map_sql_idmap_test', 'rollback_action', $field_schema);
$schema->expects($this->at(3))
->method('fieldExists')
->with('migrate_map_sql_idmap_test', 'hash')
->will($this->returnValue(FALSE));
$field_schema = array(
'type' => 'varchar',
'length' => '64',
'not null' => FALSE,
'description' => 'Hash of source row data, for detecting changes',
);
$schema->expects($this->at(4))
->method('addField')
->with('migrate_map_sql_idmap_test', 'hash', $field_schema);
$schema->expects($this->exactly(5))
->method($this->anything());
$this->runEnsureTablesTest($schema);
}
/**
* Actually run the test.
*
* @param array $schema
* The mock schema object with expectations set. The Sql constructor calls
* ensureTables() which in turn calls this object and the expectations on
* it are the actual test and there are no additional asserts added.
*/
protected function runEnsureTablesTest($schema) {
$database = $this->getMockBuilder('Drupal\Core\Database\Connection')
->disableOriginalConstructor()
->getMock();
$database->expects($this->any())
->method('schema')
->will($this->returnValue($schema));
$migration = $this->getMigration();
$plugin = $this->getMock('Drupal\migrate\Plugin\MigrateSourceInterface');
$plugin->expects($this->any())
->method('getIds')
->will($this->returnValue(array(
'source_id_property' => array(
'type' => 'integer',
),
)));
$migration->expects($this->any())
->method('getSourcePlugin')
->will($this->returnValue($plugin));
$plugin = $this->getMock('Drupal\migrate\Plugin\MigrateSourceInterface');
$plugin->expects($this->any())
->method('getIds')
->will($this->returnValue(array(
'destination_id_property' => array(
'type' => 'string',
),
)));
$migration->expects($this->any())
->method('getDestinationPlugin')
->will($this->returnValue($plugin));
$map = new TestSqlIdMap($database, array(), 'sql', array(), $migration);
$map->getDatabase();
}
}

View file

@ -0,0 +1,798 @@
<?php
/**
* @file
* Contains \Drupal\Tests\migrate\Unit\MigrateSqlIdMapTest.
*/
namespace Drupal\Tests\migrate\Unit;
use Drupal\Core\Database\Driver\sqlite\Connection;
use Drupal\migrate\Entity\MigrationInterface;
use Drupal\migrate\MigrateException;
use Drupal\migrate\Plugin\MigrateIdMapInterface;
use Drupal\migrate\Row;
/**
* Tests the SQL ID map plugin.
*
* @group migrate
*/
class MigrateSqlIdMapTest extends MigrateTestCase {
/**
* The migration configuration, initialized to set the ID and destination IDs.
*
* @var array
*/
protected $migrationConfiguration = [
'id' => 'sql_idmap_test',
];
protected $sourceIds = [
'source_id_property' => [
'type' => 'string',
],
];
protected $destinationIds = [
'destination_id_property' => [
'type' => 'string',
],
];
/**
* The database connection.
*
* @var \Drupal\Core\Database\Connection
*/
protected $database;
public function setUp() {
$this->database = $this->getDatabase([]);
}
/**
* Saves a single ID mapping row in the database.
*
* @param array $map
* The row to save.
*/
protected function saveMap(array $map) {
$table = 'migrate_map_sql_idmap_test';
$schema = $this->database->schema();
// If the table already exists, add any columns which are in the map array,
// but don't yet exist in the table. Yay, flexibility!
if ($schema->tableExists($table)) {
foreach (array_keys($map) as $field) {
if (!$schema->fieldExists($table, $field)) {
$schema->addField($table, $field, ['type' => 'text']);
}
}
}
else {
$schema->createTable($table, $this->createSchemaFromRow($map));
}
$this->database->insert($table)->fields($map)->execute();
}
/**
* Creates a test SQL ID map plugin.
*
* @return \Drupal\Tests\migrate\Unit\TestSqlIdMap
* A SQL ID map plugin test instance.
*/
protected function getIdMap() {
$migration = $this->getMigration();
$plugin = $this->getMock('Drupal\migrate\Plugin\MigrateSourceInterface');
$plugin
->method('getIds')
->willReturn($this->sourceIds);
$migration
->method('getSourcePlugin')
->willReturn($plugin);
$plugin = $this->getMock('Drupal\migrate\Plugin\MigrateDestinationInterface');
$plugin
->method('getIds')
->willReturn($this->destinationIds);
$migration
->method('getDestinationPlugin')
->willReturn($plugin);
$id_map = new TestSqlIdMap($this->database, [], 'sql', [], $migration);
$migration
->method('getIdMap')
->willReturn($id_map);
return $id_map;
}
/**
* Sets defaults for SQL ID map plugin tests.
*/
protected function idMapDefaults() {
$defaults = array(
'source_row_status' => MigrateIdMapInterface::STATUS_IMPORTED,
'rollback_action' => MigrateIdMapInterface::ROLLBACK_DELETE,
'hash' => '',
);
// By default, the PDO SQLite driver strongly prefers to return strings
// from SELECT queries. Even for columns that don't store strings. Even
// if the connection's STRINGIFY_FETCHES attribute is FALSE. This can cause
// assertSame() calls to fail, since 0 !== '0'. Casting these values to
// strings isn't the most elegant workaround, but it allows the assertions
// to pass properly.
if ($this->database->driver() == 'sqlite') {
$defaults['source_row_status'] = (string) $defaults['source_row_status'];
$defaults['rollback_action'] = (string) $defaults['rollback_action'];
}
return $defaults;
}
/**
* Tests the ID mapping method.
*
* Create two ID mappings and update the second to verify that:
* - saving new to empty tables work.
* - saving new to nonempty tables work.
* - updating work.
*/
public function testSaveIdMapping() {
$source = array(
'source_id_property' => 'source_value',
);
$row = new Row($source, ['source_id_property' => []]);
$id_map = $this->getIdMap();
$id_map->saveIdMapping($row, ['destination_id_property' => 2]);
$expected_result = [
[
'sourceid1' => 'source_value',
'destid1' => 2,
] + $this->idMapDefaults(),
];
$this->queryResultTest($this->getIdMapContents(), $expected_result);
$source = [
'source_id_property' => 'source_value_1',
];
$row = new Row($source, ['source_id_property' => []]);
$id_map->saveIdMapping($row, ['destination_id_property' => 3]);
$expected_result[] = [
'sourceid1' => 'source_value_1',
'destid1' => 3,
] + $this->idMapDefaults();
$this->queryResultTest($this->getIdMapContents(), $expected_result);
$id_map->saveIdMapping($row, ['destination_id_property' => 4]);
$expected_result[1]['destid1'] = 4;
$this->queryResultTest($this->getIdMapContents(), $expected_result);
}
/**
* Tests the SQL ID map set message method.
*/
public function testSetMessage() {
$message = $this->getMock('Drupal\migrate\MigrateMessageInterface');
$id_map = $this->getIdMap();
$id_map->setMessage($message);
$this->assertAttributeEquals($message, 'message', $id_map);
}
/**
* Tests the clear messages method.
*/
public function testClearMessages() {
$message = 'Hello world.';
$expected_results = [0, 1, 2, 3];
$id_map = $this->getIdMap();
// Insert 4 message for later delete.
foreach ($expected_results as $key => $expected_result) {
$id_map->saveMessage([$key], $message);
}
// Truncate and check that 4 messages were deleted.
$this->assertEquals($id_map->messageCount(), 4);
$id_map->clearMessages();
$count = $id_map->messageCount();
$this->assertEquals($count, 0);
}
/**
* Tests the getRowsNeedingUpdate method for rows that need an update.
*/
public function testGetRowsNeedingUpdate() {
$id_map = $this->getIdMap();
$row_statuses = [
MigrateIdMapInterface::STATUS_IMPORTED,
MigrateIdMapInterface::STATUS_NEEDS_UPDATE,
MigrateIdMapInterface::STATUS_IGNORED,
MigrateIdMapInterface::STATUS_FAILED,
];
// Create a mapping row for each STATUS constant.
foreach ($row_statuses as $status) {
$source = ['source_id_property' => 'source_value_' . $status];
$row = new Row($source, ['source_id_property' => []]);
$destination = ['destination_id_property' => 'destination_value_' . $status];
$id_map->saveIdMapping($row, $destination, $status);
$expected_results[] = [
'sourceid1' => 'source_value_' . $status,
'destid1' => 'destination_value_' . $status,
'source_row_status' => $status,
'rollback_action' => MigrateIdMapInterface::ROLLBACK_DELETE,
'hash' => '',
];
// Assert zero rows need an update.
if ($status == MigrateIdMapInterface::STATUS_IMPORTED) {
$rows_needing_update = $id_map->getRowsNeedingUpdate(1);
$this->assertCount(0, $rows_needing_update);
}
}
// Assert that test values exist.
$this->queryResultTest($this->getIdMapContents(), $expected_results);
// Assert a single row needs an update.
$row_needing_update = $id_map->getRowsNeedingUpdate(1);
$this->assertCount(1, $row_needing_update);
// Assert the row matches its original source.
$source_id = $expected_results[MigrateIdMapInterface::STATUS_NEEDS_UPDATE]['sourceid1'];
$test_row = $id_map->getRowBySource([$source_id]);
// $row_needing_update is an array of objects returned from the database,
// but $test_row is an array, so the cast is necessary.
$this->assertSame($test_row, (array) $row_needing_update[0]);
// Add additional row that needs an update.
$source = ['source_id_property' => 'source_value_multiple'];
$row = new Row($source, ['source_id_property' => []]);
$destination = ['destination_id_property' => 'destination_value_multiple'];
$id_map->saveIdMapping($row, $destination, MigrateIdMapInterface::STATUS_NEEDS_UPDATE);
// Assert multiple rows need an update.
$rows_needing_update = $id_map->getRowsNeedingUpdate(2);
$this->assertCount(2, $rows_needing_update);
}
/**
* Tests the SQL ID map message count method by counting and saving messages.
*/
public function testMessageCount() {
$message = 'Hello world.';
$expected_results = [0, 1, 2, 3];
$id_map = $this->getIdMap();
// Test count message multiple times starting from 0.
foreach ($expected_results as $key => $expected_result) {
$count = $id_map->messageCount();
$this->assertEquals($expected_result, $count);
$id_map->saveMessage([$key], $message);
}
}
/**
* Tests the SQL ID map save message method.
*/
public function testMessageSave() {
$message = 'Hello world.';
$expected_results = [
1 => ['message' => $message, 'level' => MigrationInterface::MESSAGE_ERROR],
2 => ['message' => $message, 'level' => MigrationInterface::MESSAGE_WARNING],
3 => ['message' => $message, 'level' => MigrationInterface::MESSAGE_NOTICE],
4 => ['message' => $message, 'level' => MigrationInterface::MESSAGE_INFORMATIONAL],
];
$id_map = $this->getIdMap();
foreach ($expected_results as $key => $expected_result) {
$id_map->saveMessage([$key], $message, $expected_result['level']);
$message_row = $this->database->select($id_map->messageTableName(), 'message')
->fields('message')
->condition('level', $expected_result['level'])
->condition('message', $expected_result['message'])
->execute()
->fetchAssoc();
$this->assertEquals($expected_result['message'], $message_row['message'], 'Message from database was read.');
}
// Insert with default level.
$message_default = 'Hello world default.';
$id_map->saveMessage([5], $message_default);
$message_row = $this->database->select($id_map->messageTableName(), 'message')
->fields('message')
->condition('level', MigrationInterface::MESSAGE_ERROR)
->condition('message', $message_default)
->execute()
->fetchAssoc();
$this->assertEquals($message_default, $message_row['message'], 'Message from database was read.');
}
/**
* Tests the getRowBySource method.
*/
public function testGetRowBySource() {
$this->getDatabase([]);
$row = [
'sourceid1' => 'source_id_value_1',
'sourceid2' => 'source_id_value_2',
'destid1' => 'destination_id_value_1',
] + $this->idMapDefaults();
$this->saveMap($row);
$row = [
'sourceid1' => 'source_id_value_3',
'sourceid2' => 'source_id_value_4',
'destid1' => 'destination_id_value_2',
] + $this->idMapDefaults();
$this->saveMap($row);
$source_id_values = [$row['sourceid1'], $row['sourceid2']];
$id_map = $this->getIdMap();
$result_row = $id_map->getRowBySource($source_id_values);
$this->assertSame($row, $result_row);
$source_id_values = ['missing_value_1', 'missing_value_2'];
$result_row = $id_map->getRowBySource($source_id_values);
$this->assertFalse($result_row);
}
/**
* Data provider for testLookupDestinationIdMapping().
*
* Scenarios to test (for both hits and misses) are:
* - Single-value source ID to single-value destination ID.
* - Multi-value source ID to multi-value destination ID.
* - Single-value source ID to multi-value destination ID.
* - Multi-value source ID to single-value destination ID.
*/
public function lookupDestinationIdMappingDataProvider() {
return [
[1, 1],
[2, 2],
[1, 2],
[2, 1],
];
}
/**
* Performs destination ID test on source and destination fields.
*
* @param int $num_source_fields
* Number of source fields to test.
* @param int $num_destination_fields
* Number of destination fields to test.
* @dataProvider lookupDestinationIdMappingDataProvider
*/
public function testLookupDestinationIdMapping($num_source_fields, $num_destination_fields) {
// Adjust the migration configuration according to the number of source and
// destination fields.
$this->sourceIds = [];
$this->destinationIds = [];
$source_id_values = [];
$nonexistent_id_values = [];
$row = $this->idMapDefaults();
for ($i = 1; $i <= $num_source_fields; $i++) {
$row["sourceid$i"] = "source_id_value_$i";
$source_id_values[] = "source_id_value_$i";
$nonexistent_id_values[] = "nonexistent_source_id_value_$i";
$this->sourceIds["source_id_property_$i"] = [];
}
$expected_result = [];
for ($i = 1; $i <= $num_destination_fields; $i++) {
$row["destid$i"] = "destination_id_value_$i";
$expected_result[] = "destination_id_value_$i";
$this->destinationIds["destination_id_property_$i"] = [];
}
$this->saveMap($row);
$id_map = $this->getIdMap();
// Test for a valid hit.
$destination_id = $id_map->lookupDestinationId($source_id_values);
$this->assertSame($expected_result, $destination_id);
// Test for a miss.
$destination_id = $id_map->lookupDestinationId($nonexistent_id_values);
$this->assertSame(0, count($destination_id));
}
/**
* Tests the getRowByDestination method.
*/
public function testGetRowByDestination() {
$row = [
'sourceid1' => 'source_id_value_1',
'sourceid2' => 'source_id_value_2',
'destid1' => 'destination_id_value_1',
] + $this->idMapDefaults();
$this->saveMap($row);
$row = [
'sourceid1' => 'source_id_value_3',
'sourceid2' => 'source_id_value_4',
'destid1' => 'destination_id_value_2',
] + $this->idMapDefaults();
$this->saveMap($row);
$dest_id_values = [$row['destid1']];
$id_map = $this->getIdMap();
$result_row = $id_map->getRowByDestination($dest_id_values);
$this->assertSame($row, $result_row);
// This value does not exist.
$dest_id_values = ['invalid_destination_id_property'];
$id_map = $this->getIdMap();
$result_row = $id_map->getRowByDestination($dest_id_values);
$this->assertFalse($result_row);
}
/**
* Data provider for testLookupSourceIDMapping().
*
* Scenarios to test (for both hits and misses) are:
* - Single-value destination ID to single-value source ID.
* - Multi-value destination ID to multi-value source ID.
* - Single-value destination ID to multi-value source ID.
* - Multi-value destination ID to single-value source ID.
*/
public function lookupSourceIDMappingDataProvider() {
return [
[1, 1],
[2, 2],
[1, 2],
[2, 1],
];
}
/**
* Performs the source ID test on source and destination fields.
*
* @param int $num_source_fields
* Number of source fields to test.
* @param int $num_destination_fields
* Number of destination fields to test.
* @dataProvider lookupSourceIDMappingDataProvider
*/
public function testLookupSourceIDMapping($num_source_fields, $num_destination_fields) {
// Adjust the migration configuration according to the number of source and
// destination fields.
$this->sourceIds = [];
$this->destinationIds = [];
$row = $this->idMapDefaults();
$expected_result = [];
for ($i = 1; $i <= $num_source_fields; $i++) {
$row["sourceid$i"] = "source_id_value_$i";
$expected_result[] = "source_id_value_$i";
$this->sourceIds["source_id_property_$i"] = [];
}
$destination_id_values = [];
$nonexistent_id_values = [];
for ($i = 1; $i <= $num_destination_fields; $i++) {
$row["destid$i"] = "destination_id_value_$i";
$destination_id_values[] = "destination_id_value_$i";
$nonexistent_id_values[] = "nonexistent_destination_id_value_$i";
$this->destinationIds["destination_id_property_$i"] = [];
}
$this->saveMap($row);
$id_map = $this->getIdMap();
// Test for a valid hit.
$source_id = $id_map->lookupSourceID($destination_id_values);
$this->assertSame($expected_result, $source_id);
// Test for a miss.
$source_id = $id_map->lookupSourceID($nonexistent_id_values);
$this->assertSame(0, count($source_id));
}
/**
* Tests the imported count method.
*
* Scenarios to test for:
* - No imports.
* - One import.
* - Multiple imports.
*/
public function testImportedCount() {
$id_map = $this->getIdMap();
// Add a single failed row and assert zero imported rows.
$source = ['source_id_property' => 'source_value_failed'];
$row = new Row($source, ['source_id_property' => []]);
$destination = ['destination_id_property' => 'destination_value_failed'];
$id_map->saveIdMapping($row, $destination, MigrateIdMapInterface::STATUS_FAILED);
$this->assertSame(0, (int) $id_map->importedCount());
// Add an imported row and assert single count.
$source = ['source_id_property' => 'source_value_imported'];
$row = new Row($source, ['source_id_property' => []]);
$destination = ['destination_id_property' => 'destination_value_imported'];
$id_map->saveIdMapping($row, $destination, MigrateIdMapInterface::STATUS_IMPORTED);
$this->assertSame(1, (int) $id_map->importedCount());
// Add a row needing update and assert multiple imported rows.
$source = ['source_id_property' => 'source_value_update'];
$row = new Row($source, ['source_id_property' => []]);
$destination = ['destination_id_property' => 'destination_value_update'];
$id_map->saveIdMapping($row, $destination, MigrateIdMapInterface::STATUS_NEEDS_UPDATE);
$this->assertSame(2, (int) $id_map->importedCount());
}
/**
* Tests the number of processed source rows.
*
* Scenarios to test for:
* - No processed rows.
* - One processed row.
* - Multiple processed rows.
*/
public function testProcessedCount() {
$id_map = $this->getIdMap();
// Assert zero rows have been processed before adding rows.
$this->assertSame(0, (int) $id_map->processedCount());
$row_statuses = [
MigrateIdMapInterface::STATUS_IMPORTED,
MigrateIdMapInterface::STATUS_NEEDS_UPDATE,
MigrateIdMapInterface::STATUS_IGNORED,
MigrateIdMapInterface::STATUS_FAILED,
];
// Create a mapping row for each STATUS constant.
foreach ($row_statuses as $status) {
$source = ['source_id_property' => 'source_value_' . $status];
$row = new Row($source, ['source_id_property' => []]);
$destination = ['destination_id_property' => 'destination_value_' . $status];
$id_map->saveIdMapping($row, $destination, $status);
if ($status == MigrateIdMapInterface::STATUS_IMPORTED) {
// Assert a single row has been processed.
$this->assertSame(1, (int) $id_map->processedCount());
}
}
// Assert multiple rows have been processed.
$this->assertSame(count($row_statuses), (int) $id_map->processedCount());
}
/**
* Data provider for testUpdateCount().
*
* Scenarios to test for:
* - No updates.
* - One update.
* - Multiple updates.
*/
public function updateCountDataProvider() {
return [
[0],
[1],
[3],
];
}
/**
* Performs the update count test with a given number of update rows.
*
* @param int $num_update_rows
* The number of update rows to test.
* @dataProvider updateCountDataProvider
*/
public function testUpdateCount($num_update_rows) {
for ($i = 0; $i < 5; $i++) {
$row = $this->idMapDefaults();
$row['sourceid1'] = "source_id_value_$i";
$row['destid1'] = "destination_id_value_$i";
$row['source_row_status'] = MigrateIdMapInterface::STATUS_IMPORTED;
$this->saveMap($row);
}
for (; $i < 5 + $num_update_rows; $i++) {
$row = $this->idMapDefaults();
$row['sourceid1'] = "source_id_value_$i";
$row['destid1'] = "destination_id_value_$i";
$row['source_row_status'] = MigrateIdMapInterface::STATUS_NEEDS_UPDATE;
$this->saveMap($row);
}
$id_map = $this->getIdMap();
$this->assertSame($num_update_rows, (int) $id_map->updateCount());
}
/**
* Data provider for testErrorCount().
*
* Scenarios to test for:
* - No errors.
* - One error.
* - Multiple errors.
*/
public function errorCountDataProvider() {
return [
[0],
[1],
[3],
];
}
/**
* Performs error count test with a given number of error rows.
*
* @param int $num_error_rows
* Number of error rows to test.
* @dataProvider errorCountDataProvider
*/
public function testErrorCount($num_error_rows) {
for ($i = 0; $i < 5; $i++) {
$row = $this->idMapDefaults();
$row['sourceid1'] = "source_id_value_$i";
$row['destid1'] = "destination_id_value_$i";
$row['source_row_status'] = MigrateIdMapInterface::STATUS_IMPORTED;
$this->saveMap($row);
}
for (; $i < 5 + $num_error_rows; $i++) {
$row = $this->idMapDefaults();
$row['sourceid1'] = "source_id_value_$i";
$row['destid1'] = "destination_id_value_$i";
$row['source_row_status'] = MigrateIdMapInterface::STATUS_FAILED;
$this->saveMap($row);
}
$this->assertSame($num_error_rows, (int) $this->getIdMap()->errorCount());
}
/**
* Tests setting a row source_row_status to STATUS_NEEDS_UPDATE.
*/
public function testSetUpdate() {
$id_map = $this->getIdMap();
$row_statuses = [
MigrateIdMapInterface::STATUS_IMPORTED,
MigrateIdMapInterface::STATUS_NEEDS_UPDATE,
MigrateIdMapInterface::STATUS_IGNORED,
MigrateIdMapInterface::STATUS_FAILED,
];
// Create a mapping row for each STATUS constant.
foreach ($row_statuses as $status) {
$source = ['source_id_property' => 'source_value_' . $status];
$row = new Row($source, ['source_id_property' => []]);
$destination = ['destination_id_property' => 'destination_value_' . $status];
$id_map->saveIdMapping($row, $destination, $status);
$expected_results[] = [
'sourceid1' => 'source_value_' . $status,
'destid1' => 'destination_value_' . $status,
'source_row_status' => $status,
'rollback_action' => MigrateIdMapInterface::ROLLBACK_DELETE,
'hash' => '',
];
}
// Assert that test values exist.
$this->queryResultTest($this->getIdMapContents(), $expected_results);
// Mark each row as STATUS_NEEDS_UPDATE.
foreach ($row_statuses as $status) {
$id_map->setUpdate(['source_value_' . $status]);
}
// Update expected results.
foreach ($expected_results as $key => $value) {
$expected_results[$key]['source_row_status'] = MigrateIdMapInterface::STATUS_NEEDS_UPDATE;
}
// Assert that updated expected values match.
$this->queryResultTest($this->getIdMapContents(), $expected_results);
// Assert an exception is thrown when source identifiers are not provided.
try {
$id_map->setUpdate([]);
$this->assertFalse(FALSE, 'MigrateException not thrown, when source identifiers were provided to update.');
}
catch (MigrateException $e) {
$this->assertTrue(TRUE, "MigrateException thrown, when source identifiers were not provided to update.");
}
}
/**
* Tests prepareUpdate().
*/
public function testPrepareUpdate() {
$id_map = $this->getIdMap();
$row_statuses = [
MigrateIdMapInterface::STATUS_IMPORTED,
MigrateIdMapInterface::STATUS_NEEDS_UPDATE,
MigrateIdMapInterface::STATUS_IGNORED,
MigrateIdMapInterface::STATUS_FAILED,
];
// Create a mapping row for each STATUS constant.
foreach ($row_statuses as $status) {
$source = ['source_id_property' => 'source_value_' . $status];
$row = new Row($source, ['source_id_property' => []]);
$destination = ['destination_id_property' => 'destination_value_' . $status];
$id_map->saveIdMapping($row, $destination, $status);
$expected_results[] = [
'sourceid1' => 'source_value_' . $status,
'destid1' => 'destination_value_' . $status,
'source_row_status' => $status,
'rollback_action' => MigrateIdMapInterface::ROLLBACK_DELETE,
'hash' => '',
];
}
// Assert that test values exist.
$this->queryResultTest($this->getIdMapContents(), $expected_results);
// Mark all rows as STATUS_NEEDS_UPDATE.
$id_map->prepareUpdate();
// Update expected results.
foreach ($expected_results as $key => $value) {
$expected_results[$key]['source_row_status'] = MigrateIdMapInterface::STATUS_NEEDS_UPDATE;
}
// Assert that updated expected values match.
$this->queryResultTest($this->getIdMapContents(), $expected_results);
}
/**
* Tests the destroy method.
*
* Scenarios to test for:
* - No errors.
* - One error.
* - Multiple errors.
*/
public function testDestroy() {
$id_map = $this->getIdMap();
// Initialize the id map.
$id_map->getDatabase();
$map_table_name = $id_map->mapTableName();
$message_table_name = $id_map->messageTableName();
$row = new Row(['source_id_property' => 'source_value'], ['source_id_property' => []]);
$id_map->saveIdMapping($row, ['destination_id_property' => 2]);
$id_map->saveMessage(['source_value'], 'A message');
$this->assertTrue($this->database->schema()->tableExists($map_table_name),
"$map_table_name exists");
$this->assertTrue($this->database->schema()->tableExists($message_table_name),
"$message_table_name exists");
$id_map->destroy();
$this->assertFalse($this->database->schema()->tableExists($map_table_name),
"$map_table_name does not exist");
$this->assertFalse($this->database->schema()->tableExists($message_table_name),
"$message_table_name does not exist");
}
/**
* Tests the getQualifiedMapTable method with a prefixed database.
*/
public function testGetQualifiedMapTablePrefix() {
$connection_options = [
'database' => ':memory:',
'prefix' => 'prefix',
];
$pdo = Connection::open($connection_options);
$this->database = new Connection($pdo, $connection_options);
$qualified_map_table = $this->getIdMap()->getQualifiedMapTableName();
// The SQLite driver is a special flower. It will prefix tables with
// PREFIX.TABLE, instead of the standard PREFIXTABLE.
// @see \Drupal\Core\Database\Driver\sqlite\Connection::__construct()
$this->assertEquals('prefix.migrate_map_sql_idmap_test', $qualified_map_table);
}
/**
* Tests all the iterator methods in one swing.
*
* The iterator methods are:
* - Sql::rewind()
* - Sql::next()
* - Sql::valid()
* - Sql::key()
* - Sql::current()
*/
public function testIterators() {
for ($i = 0; $i < 3; $i++) {
$row = $this->idMapDefaults();
$row['sourceid1'] = "source_id_value_$i";
$row['destid1'] = "destination_id_value_$i";
$row['source_row_status'] = MigrateIdMapInterface::STATUS_IMPORTED;
$expected_results[serialize(['sourceid1' => $row['sourceid1']])] = ['destid1' => $row['destid1']];
$this->saveMap($row);
}
$this->assertSame(iterator_to_array($this->getIdMap()), $expected_results);
}
private function getIdMapContents() {
$result = $this->database
->select('migrate_map_sql_idmap_test', 't')
->fields('t')
->execute();
// The return value needs to be countable, or it will fail certain
// assertions. iterator_to_array() will not suffice because it won't
// respect the PDO fetch mode, if specified.
$contents = [];
foreach ($result as $row) {
$contents[] = (array) $row;
}
return $contents;
}
}

View file

@ -0,0 +1,118 @@
<?php
/**
* @file
* Contains \Drupal\Tests\migrate\Unit\MigrateSqlSourceTestCase.
*/
namespace Drupal\Tests\migrate\Unit;
/**
* Base class for Migrate module source unit tests.
*/
abstract class MigrateSqlSourceTestCase extends MigrateTestCase {
/**
* The tested source plugin.
*
* @var \Drupal\migrate_drupal\Plugin\migrate\source\DrupalSqlBase.
*/
protected $source;
/**
* The database contents.
*
* Database contents represents a mocked database. It should contain an
* associative array with the table name as key, and as many nested arrays as
* the number of mocked rows. Each of those faked rows must be another array
* with the column name as the key and the value as the cell.
*
* @var array
*/
protected $databaseContents = array();
/**
* The plugin class under test.
*
* The plugin system is not working during unit testing so the source plugin
* class needs to be manually specified.
*
* @var string
*/
const PLUGIN_CLASS = '';
/**
* The high water mark at the beginning of the import operation.
*
* Once the migration is run, we save a mark of the migrated sources, so the
* migration can run again and update only new sources or changed sources.
*
* @var string
*/
const ORIGINAL_HIGH_WATER = '';
/**
* Expected results after the source parsing.
*
* @var array
*/
protected $expectedResults = array();
/**
* The source plugin instance under test.
*
* @var \Drupal\migrate\Plugin\MigrateSourceInterface
*/
protected $plugin;
/**
* {@inheritdoc}
*/
protected function setUp() {
$module_handler = $this->getMock('Drupal\Core\Extension\ModuleHandlerInterface');
$entity_manager = $this->getMock('Drupal\Core\Entity\EntityManagerInterface');
$migration = $this->getMigration();
$migration->expects($this->any())
->method('getHighWater')
->will($this->returnValue(static::ORIGINAL_HIGH_WATER));
// Setup the plugin.
$plugin_class = static::PLUGIN_CLASS;
$plugin = new $plugin_class($this->migrationConfiguration['source'], $this->migrationConfiguration['source']['plugin'], array(), $migration, $entity_manager);
// Do some reflection to set the database and moduleHandler.
$plugin_reflection = new \ReflectionClass($plugin);
$database_property = $plugin_reflection->getProperty('database');
$database_property->setAccessible(TRUE);
$module_handler_property = $plugin_reflection->getProperty('moduleHandler');
$module_handler_property->setAccessible(TRUE);
// Set the database and the module handler onto our plugin.
$database_property->setValue($plugin, $this->getDatabase($this->databaseContents + array('test_map' => array())));
$module_handler_property->setValue($plugin, $module_handler);
$plugin->setStringTranslation($this->getStringTranslationStub());
$migration->expects($this->any())
->method('getSourcePlugin')
->will($this->returnValue($plugin));
$this->source = $plugin;
}
/**
* Test the source returns the same rows as expected.
*/
public function testRetrieval() {
$this->queryResultTest($this->source, $this->expectedResults);
}
/**
* @param \Drupal\migrate\Row $row
* @param string $key
* @return mixed
*/
protected function getValue($row, $key) {
return $row->getSourceProperty($key);
}
}

View file

@ -0,0 +1,162 @@
<?php
/**
* @file
* Contains \Drupal\Tests\migrate\Unit\MigrateTestCase.
*/
namespace Drupal\Tests\migrate\Unit;
use Drupal\Core\Database\Driver\sqlite\Connection;
use Drupal\Core\DependencyInjection\ContainerBuilder;
use Drupal\Tests\UnitTestCase;
/**
* Provides setup and helper methods for Migrate module tests.
*/
abstract class MigrateTestCase extends UnitTestCase {
protected $migrationConfiguration = [];
/**
* Retrieve a mocked migration.
*
* @return \Drupal\migrate\Entity\MigrationInterface|\PHPUnit_Framework_MockObject_MockObject
* The mocked migration.
*/
protected function getMigration() {
$this->migrationConfiguration += ['migrationClass' => 'Drupal\migrate\Entity\Migration'];
$this->idMap = $this->getMock('Drupal\migrate\Plugin\MigrateIdMapInterface');
$this->idMap->expects($this->any())
->method('getQualifiedMapTableName')
->will($this->returnValue('test_map'));
$migration = $this->getMockBuilder($this->migrationConfiguration['migrationClass'])
->disableOriginalConstructor()
->getMock();
$migration->expects($this->any())
->method('checkRequirements')
->will($this->returnValue(TRUE));
$migration->expects($this->any())
->method('getIdMap')
->will($this->returnValue($this->idMap));
$configuration = &$this->migrationConfiguration;
$migration->expects($this->any())->method('get')->will($this->returnCallback(function ($argument) use (&$configuration) {
return isset($configuration[$argument]) ? $configuration[$argument] : '';
}));
$migration->expects($this->any())->method('set')->will($this->returnCallback(function ($argument, $value) use (&$configuration) {
$configuration[$argument] = $value;
}));
$migration->expects($this->any())
->method('id')
->will($this->returnValue($configuration['id']));
return $migration;
}
/**
* Get an SQLite database connection object for use in tests.
*
* @param array $database_contents
* The database contents faked as an array. Each key is a table name, each
* value is a list of table rows, an associative array of field => value.
* @param array $connection_options
* (optional) Options for the database connection.
*
* @return \Drupal\Core\Database\Driver\sqlite\Connection
* The database connection.
*/
protected function getDatabase(array $database_contents, $connection_options = []) {
if (extension_loaded('pdo_sqlite')) {
$connection_options['database'] = ':memory:';
$pdo = Connection::open($connection_options);
$connection = new Connection($pdo, $connection_options);
}
else {
$this->markTestSkipped('The pdo_sqlite extension is not available.');
}
// Initialize the DIC with a fake module handler for alterable queries.
$container = new ContainerBuilder();
$container->set('module_handler', $this->getMock('\Drupal\Core\Extension\ModuleHandlerInterface'));
\Drupal::setContainer($container);
// Create the tables and load them up with data, skipping empty ones.
foreach (array_filter($database_contents) as $table => $rows) {
$pilot_row = reset($rows);
$connection->schema()->createTable($table, $this->createSchemaFromRow($pilot_row));
$insert = $connection->insert($table)->fields(array_keys($pilot_row));
array_walk($rows, [$insert, 'values']);
$insert->execute();
}
return $connection;
}
/**
* Generates a table schema from a row.
*
* @param array $row
* The reference row on which to base the schema.
*
* @return array
* The Schema API-ready table schema.
*/
protected function createSchemaFromRow(array $row) {
// SQLite uses loose ("affinity") typing, so it's OK for every column
// to be a text field.
$fields = array_map(function() { return ['type' => 'text']; }, $row);
return ['fields' => $fields];
}
/**
* Tests a query
*
* @param array|\Traversable
* The countable. foreach-able actual results if a query is being run.
*/
public function queryResultTest($iter, $expected_results) {
$this->assertSame(count($expected_results), count($iter), 'Number of results match');
$count = 0;
foreach ($iter as $data_row) {
$expected_row = $expected_results[$count];
$count++;
foreach ($expected_row as $key => $expected_value) {
$this->retrievalAssertHelper($expected_value, $this->getValue($data_row, $key), sprintf('Value matches for key "%s"', $key));
}
}
$this->assertSame(count($expected_results), $count);
}
/**
* @param array $row
* @param string $key
* @return mixed
*/
protected function getValue($row, $key) {
return $row[$key];
}
/**
* Asserts tested values during test retrieval.
*
* @param mixed $expected_value
* The incoming expected value to test.
* @param mixed $actual_value
* The incoming value itself.
* @param string $message
* The tested result as a formatted string.
*/
protected function retrievalAssertHelper($expected_value, $actual_value, $message) {
if (is_array($expected_value)) {
foreach ($expected_value as $k => $v) {
$this->retrievalAssertHelper($v, $actual_value[$k], $message . '[' . $k . ']');
}
}
else {
$this->assertSame((string) $expected_value, (string) $actual_value, $message);
}
}
}

View file

@ -0,0 +1,148 @@
<?php
/**
* @file
* Contains \Drupal\Tests\migrate\Unit\MigrationTest.
*/
namespace Drupal\Tests\migrate\Unit;
use Drupal\Core\Entity\EntityManagerInterface;
use Drupal\migrate\Entity\Migration;
use Drupal\migrate\Exception\RequirementsException;
use Drupal\migrate\Plugin\MigrateDestinationInterface;
use Drupal\migrate\Plugin\MigrateSourceInterface;
use Drupal\migrate\Plugin\RequirementsInterface;
use Drupal\Tests\UnitTestCase;
/**
* @coversDefaultClass \Drupal\migrate\Entity\Migration
* @group Migration
*/
class MigrationTest extends UnitTestCase {
/**
* Tests checking requirements for source plugins.
*
* @covers ::checkRequirements
*
* @expectedException \Drupal\migrate\Exception\RequirementsException
* @expectedExceptionMessage Missing source requirement
*/
public function testRequirementsForSourcePlugin() {
$migration = new TestMigration();
$source_plugin = $this->getMock('Drupal\Tests\migrate\Unit\RequirementsAwareSourceInterface');
$source_plugin->expects($this->once())
->method('checkRequirements')
->willThrowException(new RequirementsException('Missing source requirement', ['key' => 'value']));
$destination_plugin = $this->getMock('Drupal\Tests\migrate\Unit\RequirementsAwareDestinationInterface');
$migration->setSourcePlugin($source_plugin);
$migration->setDestinationPlugin($destination_plugin);
$migration->checkRequirements();
}
/**
* Tests checking requirements for destination plugins.
*
* @covers ::checkRequirements
*
* @expectedException \Drupal\migrate\Exception\RequirementsException
* @expectedExceptionMessage Missing destination requirement
*/
public function testRequirementsForDestinationPlugin() {
$migration = new TestMigration();
$source_plugin = $this->getMock('Drupal\migrate\Plugin\MigrateSourceInterface');
$destination_plugin = $this->getMock('Drupal\Tests\migrate\Unit\RequirementsAwareDestinationInterface');
$destination_plugin->expects($this->once())
->method('checkRequirements')
->willThrowException(new RequirementsException('Missing destination requirement', ['key' => 'value']));
$migration->setSourcePlugin($source_plugin);
$migration->setDestinationPlugin($destination_plugin);
$migration->checkRequirements();
}
/**
* Tests checking requirements for destination plugins.
*
* @covers ::checkRequirements
*
* @expectedException \Drupal\migrate\Exception\RequirementsException
* @expectedExceptionMessage Missing migrations test_a, test_c
*/
public function testRequirementsForMigrations() {
$migration = new TestMigration();
// Setup source and destination plugins without any requirements.
$source_plugin = $this->getMock('Drupal\migrate\Plugin\MigrateSourceInterface');
$destination_plugin = $this->getMock('Drupal\migrate\Plugin\MigrateDestinationInterface');
$migration->setSourcePlugin($source_plugin);
$migration->setDestinationPlugin($destination_plugin);
$entity_manager = $this->getMock('Drupal\Core\Entity\EntityManagerInterface');
$migration->setEntityManager($entity_manager);
// We setup the requirements that test_a doesn't exist and test_c is not
// completed yet.
$migration->setRequirements(['test_a', 'test_b', 'test_c', 'test_d']);
$migration_b = $this->getMock('Drupal\migrate\Entity\MigrationInterface');
$migration_c = $this->getMock('Drupal\migrate\Entity\MigrationInterface');
$migration_d = $this->getMock('Drupal\migrate\Entity\MigrationInterface');
$migration_b->expects($this->once())
->method('isComplete')
->willReturn(TRUE);
$migration_c->expects($this->once())
->method('isComplete')
->willReturn(FALSE);
$migration_d->expects($this->once())
->method('isComplete')
->willReturn(TRUE);
$migration_storage = $this->getMock('Drupal\Core\Entity\EntityStorageInterface');
$migration_storage->expects($this->once())
->method('loadMultiple')
->with(['test_a', 'test_b', 'test_c', 'test_d'])
->willReturn(['test_b' => $migration_b, 'test_c' => $migration_c, 'test_d' => $migration_d]);
$entity_manager->expects($this->once())
->method('getStorage')
->with('migration')
->willReturn($migration_storage);
$migration->checkRequirements();
}
}
class TestMigration extends Migration {
public function __construct() {
}
public function setRequirements(array $requirements) {
$this->requirements = $requirements;
}
public function setSourcePlugin(MigrateSourceInterface $source_plugin) {
$this->sourcePlugin = $source_plugin;
}
public function setDestinationPlugin(MigrateDestinationInterface $destination_plugin) {
$this->destinationPlugin = $destination_plugin;
}
public function setEntityManager(EntityManagerInterface $entity_manager) {
$this->entityManager = $entity_manager;
}
}
interface RequirementsAwareSourceInterface extends MigrateSourceInterface, RequirementsInterface {}
interface RequirementsAwareDestinationInterface extends MigrateDestinationInterface, RequirementsInterface {}

View file

@ -0,0 +1,250 @@
<?php
/**
* @file
* Contains \Drupal\Tests\migrate\Unit\RowTest.
*/
namespace Drupal\Tests\migrate\Unit;
use Drupal\migrate\Plugin\MigrateIdMapInterface;
use Drupal\migrate\Row;
use Drupal\Tests\UnitTestCase;
/**
* @coversDefaultClass \Drupal\migrate\Row
* @group migrate
*/
class RowTest extends UnitTestCase {
/**
* The source IDs.
*
* @var array
*/
protected $testSourceIds = array(
'nid' => 'Node ID',
);
/**
* The test values.
*
* @var array
*/
protected $testValues = array(
'nid' => 1,
'title' => 'node 1',
);
/**
* The test hash.
*
* @var string
*/
protected $testHash = '85795d4cde4a2425868b812cc88052ecd14fc912e7b9b4de45780f66750e8b1e';
/**
* The test hash after changing title value to 'new title'.
*
* @var string
*/
protected $testHashMod = '9476aab0b62b3f47342cc6530441432e5612dcba7ca84115bbab5cceaca1ecb3';
/**
* Tests object creation: empty.
*/
public function testRowWithoutData() {
$row = new Row(array(), array());
$this->assertSame(array(), $row->getSource(), 'Empty row');
}
/**
* Tests object creation: basic.
*/
public function testRowWithBasicData() {
$row = new Row($this->testValues, $this->testSourceIds);
$this->assertSame($this->testValues, $row->getSource(), 'Row with data, simple id.');
}
/**
* Tests object creation: multiple source IDs.
*/
public function testRowWithMultipleSourceIds() {
$multi_source_ids = $this->testSourceIds + array('vid' => 'Node revision');
$multi_source_ids_values = $this->testValues + array('vid' => 1);
$row = new Row($multi_source_ids_values, $multi_source_ids);
$this->assertSame($multi_source_ids_values, $row->getSource(), 'Row with data, multifield id.');
}
/**
* Tests object creation: invalid values.
*
* @expectedException \Exception
*/
public function testRowWithInvalidData() {
$invalid_values = array(
'title' => 'node X',
);
$row = new Row($invalid_values, $this->testSourceIds);
}
/**
* Tests source immutability after freeze.
*
* @expectedException \Exception
*/
public function testSourceFreeze() {
$row = new Row($this->testValues, $this->testSourceIds);
$row->rehash();
$this->assertSame($this->testHash, $row->getHash(), 'Correct hash.');
$row->setSourceProperty('title', 'new title');
$row->rehash();
$this->assertSame($this->testHashMod, $row->getHash(), 'Hash changed correctly.');
$row->freezeSource();
$row->setSourceProperty('title', 'new title');
}
/**
* Tests setting on a frozen row.
*
* @expectedException \Exception
* @expectedExceptionMessage The source is frozen and can't be changed any more
*/
public function testSetFrozenRow() {
$row = new Row($this->testValues, $this->testSourceIds);
$row->freezeSource();
$row->setSourceProperty('title', 'new title');
}
/**
* Tests hashing.
*/
public function testHashing() {
$row = new Row($this->testValues, $this->testSourceIds);
$this->assertSame('', $row->getHash(), 'No hash at creation');
$row->rehash();
$this->assertSame($this->testHash, $row->getHash(), 'Correct hash.');
$row->rehash();
$this->assertSame($this->testHash, $row->getHash(), 'Correct hash even doing it twice.');
// Set the map to needs update.
$test_id_map = array(
'original_hash' => '',
'hash' => '',
'source_row_status' => MigrateIdMapInterface::STATUS_NEEDS_UPDATE,
);
$row->setIdMap($test_id_map);
$this->assertTrue($row->needsUpdate());
$row->rehash();
$this->assertSame($this->testHash, $row->getHash(), 'Correct hash even if id_mpa have changed.');
$row->setSourceProperty('title', 'new title');
$row->rehash();
$this->assertSame($this->testHashMod, $row->getHash(), 'Hash changed correctly.');
// Check hash calculation algorithm.
$hash = hash('sha256', serialize($row->getSource()));
$this->assertSame($hash, $row->getHash());
// Check length of generated hash used for mapping schema.
$this->assertSame(64, strlen($row->getHash()));
// Set the map to successfully imported.
$test_id_map = array(
'original_hash' => '',
'hash' => '',
'source_row_status' => MigrateIdMapInterface::STATUS_IMPORTED,
);
$row->setIdMap($test_id_map);
$this->assertFalse($row->needsUpdate());
// Set the same hash value and ensure it was not changed.
$random = $this->randomMachineName();
$test_id_map = array(
'original_hash' => $random,
'hash' => $random,
'source_row_status' => MigrateIdMapInterface::STATUS_NEEDS_UPDATE,
);
$row->setIdMap($test_id_map);
$this->assertFalse($row->changed());
// Set different has values to ensure it is marked as changed.
$test_id_map = array(
'original_hash' => $this->randomMachineName(),
'hash' => $this->randomMachineName(),
'source_row_status' => MigrateIdMapInterface::STATUS_NEEDS_UPDATE,
);
$row->setIdMap($test_id_map);
$this->assertTrue($row->changed());
}
/**
* Tests getting/setting the ID Map.
*
* @covers ::setIdMap
* @covers ::getIdMap
*/
public function testGetSetIdMap() {
$row = new Row($this->testValues, $this->testSourceIds);
$test_id_map = array(
'original_hash' => '',
'hash' => '',
'source_row_status' => MigrateIdMapInterface::STATUS_NEEDS_UPDATE,
);
$row->setIdMap($test_id_map);
$this->assertEquals($test_id_map, $row->getIdMap());
}
/**
* Tests the source ID.
*/
public function testSourceIdValues() {
$row = new Row($this->testValues, $this->testSourceIds);
$this->assertSame(array('nid' => $this->testValues['nid']), $row->getSourceIdValues());
}
/**
* Tests getting the source property.
*
* @covers ::getSourceProperty
*/
public function testGetSourceProperty() {
$row = new Row($this->testValues, $this->testSourceIds);
$this->assertSame($this->testValues['nid'], $row->getSourceProperty('nid'));
$this->assertSame($this->testValues['title'], $row->getSourceProperty('title'));
$this->assertNull($row->getSourceProperty('non_existing'));
}
/**
* Tests setting and getting the destination.
*/
public function testDestination() {
$row = new Row($this->testValues, $this->testSourceIds);
$this->assertEmpty($row->getDestination());
$this->assertFalse($row->hasDestinationProperty('nid'));
// Set a destination.
$row->setDestinationProperty('nid', 2);
$this->assertTrue($row->hasDestinationProperty('nid'));
$this->assertEquals(array('nid' => 2), $row->getDestination());
}
/**
* Tests setting/getting multiple destination IDs.
*/
public function testMultipleDestination() {
$row = new Row($this->testValues, $this->testSourceIds);
// Set some deep nested values.
$row->setDestinationProperty('image/alt', 'alt text');
$row->setDestinationProperty('image/fid', 3);
$this->assertTrue($row->hasDestinationProperty('image'));
$this->assertFalse($row->hasDestinationProperty('alt'));
$this->assertFalse($row->hasDestinationProperty('fid'));
$destination = $row->getDestination();
$this->assertEquals('alt text', $destination['image']['alt']);
$this->assertEquals(3, $destination['image']['fid']);
$this->assertEquals('alt text', $row->getDestinationProperty('image/alt'));
$this->assertEquals(3, $row->getDestinationProperty('image/fid'));
}
}

View file

@ -0,0 +1,177 @@
<?php
/**
* @file
* Contains \Drupal\Tests\migrate\Unit\SqlBaseTest.
*/
namespace Drupal\Tests\migrate\Unit;
use Drupal\migrate\Plugin\migrate\source\SqlBase;
use Drupal\Tests\UnitTestCase;
/**
* Tests the SqlBase class.
*
* @group migrate
*/
class SqlBaseTest extends UnitTestCase {
/**
* @param bool $expected_result
* The expected result.
* @param bool $id_map_is_sql
* TRUE if we want getIdMap() to return an instance of Sql.
* @param bool $with_id_map
* TRUE if we want the id map to have a valid map of ids.
* @param array $source_options
* An array of connection options for the source connection.
* @param array $idmap_options
* An array of connection options for the id map connection.
*
* @dataProvider sqlBaseTestProvider
*/
public function testMapJoinable($expected_result, $id_map_is_sql, $with_id_map, $source_options = [], $idmap_options = []) {
// Setup a connection object.
$source_connection = $this->getMockBuilder('Drupal\Core\Database\Connection')
->disableOriginalConstructor()
->getMock();
$source_connection->expects($id_map_is_sql && $with_id_map ? $this->once() : $this->never())
->method('getConnectionOptions')
->willReturn($source_options);
// Setup the id map connection.
$idmap_connection = $this->getMockBuilder('Drupal\Core\Database\Connection')
->disableOriginalConstructor()
->getMock();
$idmap_connection->expects($id_map_is_sql && $with_id_map ? $this->once() : $this->never())
->method('getConnectionOptions')
->willReturn($idmap_options);
// Setup the Sql object.
$sql = $this->getMockBuilder('Drupal\migrate\Plugin\migrate\id_map\Sql')
->disableOriginalConstructor()
->getMock();
$sql->expects($id_map_is_sql && $with_id_map ? $this->once() : $this->never())
->method('getDatabase')
->willReturn($idmap_connection);
// Setup a migration entity.
$migration = $this->getMock('Drupal\migrate\Entity\MigrationInterface');
$migration->expects($with_id_map ? $this->once() : $this->never())
->method('getIdMap')
->willReturn($id_map_is_sql ? $sql : NULL);
// Create our SqlBase test class.
$sql_base = new TestSqlBase();
$sql_base->setMigration($migration);
$sql_base->setDatabase($source_connection);
// Configure the idMap to make the check in mapJoinable() pass.
if ($with_id_map) {
$sql_base->setIds([
'uid' => ['type' => 'integer', 'alias' => 'u'],
]);
}
$this->assertEquals($expected_result, $sql_base->mapJoinable());
}
/**
* The data provider for SqlBase.
*
* @return array
* An array of data per test run.
*/
public function sqlBaseTestProvider() {
return [
// Source ids are empty so mapJoinable() is false.
[FALSE, FALSE, FALSE],
// Still false because getIdMap() is not a subclass of Sql.
[FALSE, FALSE, TRUE],
// Test mapJoinable() returns false when source and id connection options
// differ.
[FALSE, TRUE, TRUE, ['username' => 'different_from_map', 'password' => 'different_from_map'], ['username' => 'different_from_source', 'password' => 'different_from_source']],
// Returns true because source and id map connection options are the same.
[TRUE, TRUE, TRUE, ['username' => 'same_value', 'password' => 'same_value'], ['username' => 'same_value', 'password' => 'same_value']],
];
}
}
class TestSqlBase extends SqlBase {
protected $database;
protected $ids;
/**
* Override the constructor so we can create one easily.
*/
public function __construct() {}
/**
* Allows us to set the database during tests.
*
* @param mixed $database
* The database mock object.
*/
public function setDatabase($database) {
$this->database = $database;
}
/**
* {@inheritdoc}
*/
public function getDatabase() {
return $this->database;
}
/**
* Allows us to set the migration during the test.
*
* @param mixed $migration
* The migration mock.
*/
public function setMigration($migration) {
$this->migration = $migration;
}
/**
* {@inheritdoc}
*/
public function mapJoinable() {
return parent::mapJoinable();
}
/**
* {@inheritdoc}
*/
public function getIds() {
return $this->ids;
}
/**
* Allows us to set the ids during a test.
*/
public function setIds($ids) {
$this->ids = $ids;
}
/**
* {@inheritdoc}
*/
public function fields() {}
/**
* {@inheritdoc}
*/
public function query() {}
/**
* {@inheritdoc}
*/
public function calculateDependencies() {
return [];
}
}

View file

@ -0,0 +1,251 @@
<?php
/**
* @file
* Contains \Drupal\Tests\migrate\Unit\TestMigrateExecutable.
*/
namespace Drupal\Tests\migrate\Unit;
use Drupal\Core\StringTranslation\TranslationInterface;
use Drupal\migrate\MigrateExecutable;
/**
* Tests MigrateExecutable.
*/
class TestMigrateExecutable extends MigrateExecutable {
/**
* The (fake) number of seconds elapsed since the start of the test.
*
* @var int
*/
protected $timeElapsed;
/**
* The fake memory usage in bytes.
*
* @var int
*/
protected $memoryUsage;
/**
* The cleared memory usage.
*
* @var int
*/
protected $clearedMemoryUsage;
/**
* Sets the string translation service.
*
* @param \Drupal\Core\StringTranslation\TranslationInterface $string_translation
* The translation manager.
*/
public function setStringTranslation(TranslationInterface $string_translation) {
$this->stringTranslation = $string_translation;
}
/**
* Allows access to protected timeOptionExceeded method.
*
* @return bool
* A threshold exceeded value.
*/
public function timeOptionExceeded() {
return parent::timeOptionExceeded();
}
/**
* Allows access to set protected maxExecTime property.
*
* @param int $max_exec_time
* The value to set.
*/
public function setMaxExecTime($max_exec_time) {
$this->maxExecTime = $max_exec_time;
}
/**
* Allows access to protected maxExecTime property.
*
* @return int
* The value of the protected property.
*/
public function getMaxExecTime() {
return $this->maxExecTime;
}
/**
* Allows access to protected successesSinceFeedback property.
*
* @return int
* The value of the protected property.
*/
public function getSuccessesSinceFeedback() {
return $this->successesSinceFeedback;
}
/**
* Allows access to protected totalSuccesses property.
*
* @return int
* The value of the protected property.
*/
public function getTotalSuccesses() {
return $this->totalSuccesses;
}
/**
* Allows access to protected totalProcessed property.
*
* @return int
* The value of the protected property.
*/
public function getTotalProcessed() {
return $this->totalProcessed;
}
/**
* Allows access to protected processedSinceFeedback property.
*
* @return int
* The value of the protected property.
*/
public function getProcessedSinceFeedback() {
return $this->processedSinceFeedback;
}
/**
* Allows access to protected maxExecTimeExceeded method.
*
* @return bool
* The threshold exceeded value.
*/
public function maxExecTimeExceeded() {
return parent::maxExecTimeExceeded();
}
/**
* Allows access to set protected source property.
*
* @param \Drupal\migrate\Plugin\MigrateSourceInterface $source
* The value to set.
*/
public function setSource($source) {
$this->source = $source;
}
/**
* Allows access to protected sourceIdValues property.
*
* @param array $source_id_values
* The value to set.
*/
public function setSourceIdValues($source_id_values) {
$this->sourceIdValues = $source_id_values;
}
/**
* Allows setting a fake elapsed time.
*
* @param int $time
* The time in seconds.
*/
public function setTimeElapsed($time) {
$this->timeElapsed = $time;
}
/**
* {@inheritdoc}
*/
public function getTimeElapsed() {
return $this->timeElapsed;
}
/**
* {@inheritdoc}
*/
public function handleException(\Exception $exception, $save = TRUE) {
$message = $exception->getMessage();
if ($save) {
$this->saveMessage($message);
}
$this->message->display($message);
}
/**
* Allows access to the protected memoryExceeded method.
*
* @return bool
* The memoryExceeded value.
*/
public function memoryExceeded() {
return parent::memoryExceeded();
}
/**
* {@inheritdoc}
*/
protected function attemptMemoryReclaim() {
return $this->clearedMemoryUsage;
}
/**
* {@inheritdoc}
*/
protected function getMemoryUsage() {
return $this->memoryUsage;
}
/**
* Sets the fake memory usage.
*
* @param int $memory_usage
* The fake memory usage value.
* @param int $cleared_memory_usage
* (optional) The fake cleared memory value.
*/
public function setMemoryUsage($memory_usage, $cleared_memory_usage = NULL) {
$this->memoryUsage = $memory_usage;
$this->clearedMemoryUsage = $cleared_memory_usage;
}
/**
* Sets the memory limit.
*
* @param int $memory_limit
* The memory limit.
*/
public function setMemoryLimit($memory_limit) {
$this->memoryLimit = $memory_limit;
}
/**
* Sets the memory threshold.
*
* @param float $threshold
* The new threshold.
*/
public function setMemoryThreshold($threshold) {
$this->memoryThreshold = $threshold;
}
/**
* Sets the time threshold.
*
* @param float $threshold
* The new threshold.
*/
public function setTimeThreshold($threshold) {
$this->timeThreshold = $threshold;
}
/**
* {@inheritdoc}
*/
protected function formatSize($size) {
return $size;
}
}

View file

@ -0,0 +1,67 @@
<?php
/**
* @file
* Contains \Drupal\Tests\migrate\Unit\TestSqlIdMap.
*/
namespace Drupal\Tests\migrate\Unit;
use Drupal\Component\Utility\SafeMarkup;
use Drupal\Core\Database\Connection;
use Drupal\migrate\Entity\MigrationInterface;
use Drupal\migrate\MigrateException;
use Drupal\migrate\Plugin\migrate\id_map\Sql;
/**
* Defines a SQL ID map for use in tests.
*/
class TestSqlIdMap extends Sql implements \Iterator {
/**
* Constructs a TestSqlIdMap object.
*
* @param \Drupal\Core\Database\Connection $database
* The database.
* @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(Connection $database, array $configuration, $plugin_id, $plugin_definition, MigrationInterface $migration) {
$this->database = $database;
parent::__construct($configuration, $plugin_id, $plugin_definition, $migration);
}
/**
* {@inheritdoc}
*/
public function getDatabase() {
return parent::getDatabase();
}
protected function getFieldSchema(array $id_definition) {
if (!isset($id_definition['type'])) {
return array();
}
switch ($id_definition['type']) {
case 'integer':
return array(
'type' => 'int',
'not null' => TRUE,
);
case 'string':
return array(
'type' => 'varchar',
'length' => 255,
'not null' => FALSE,
);
default:
throw new MigrateException(SafeMarkup::format('@type not supported', array('@type' => $id_definition['type'])));
}
}
}

View file

@ -0,0 +1,55 @@
<?php
/**
* @file
* Contains \Drupal\Tests\migrate\Unit\destination\ConfigTest.
*/
namespace Drupal\Tests\migrate\Unit\destination;
use Drupal\migrate\Plugin\migrate\destination\Config;
use Drupal\Tests\UnitTestCase;
/**
* @coversDefaultClass \Drupal\migrate\Plugin\migrate\destination\Config
* @group migrate
*/
class ConfigTest extends UnitTestCase {
/**
* Test the import method.
*/
public function testImport() {
$source = array(
'test' => 'x',
);
$migration = $this->getMockBuilder('Drupal\migrate\Entity\Migration')
->disableOriginalConstructor()
->getMock();
$config = $this->getMockBuilder('Drupal\Core\Config\Config')
->disableOriginalConstructor()
->getMock();
foreach ($source as $key => $val) {
$config->expects($this->once())
->method('set')
->with($this->equalTo($key), $this->equalTo($val))
->will($this->returnValue($config));
}
$config->expects($this->once())
->method('save');
$config_factory = $this->getMock('Drupal\Core\Config\ConfigFactoryInterface');
$config_factory->expects($this->once())
->method('getEditable')
->with('d8_config')
->will($this->returnValue($config));
$row = $this->getMockBuilder('Drupal\migrate\Row')
->disableOriginalConstructor()
->getMock();
$row->expects($this->once())
->method('getRawDestination')
->will($this->returnValue($source));
$destination = new Config(array('config_name' => 'd8_config'), 'd8_config', array('pluginId' => 'd8_config'), $migration, $config_factory);
$destination->import($row);
}
}

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