Move into nested docroot

This commit is contained in:
Rob Davies 2017-02-13 15:31:17 +00:00
parent 83a0d3a149
commit c8b70abde9
13405 changed files with 0 additions and 0 deletions

View file

@ -0,0 +1,138 @@
<?php
/**
* @file
* Hooks provided by the Migrate module.
*/
use Drupal\migrate\Plugin\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. In the Drupal migration API the extract phase is called
* "source", the transform phase is called "process", and the load phase is
* called "destination". It is important to understand that the "load" in ETL
* means to load data into storage, while traditionally Drupal uses "load" to
* mean load data from storage into memory.
*
* In the source phase, a set of data, called the row, is retrieved from the
* data source, typically a database but it can be a CSV, JSON or XML file. The
* row is sent to the process phase where it is transformed as needed by the
* destination, or marked to be skipped. Processing can also determine that a
* stub needs to be created, for example, if a term has a parent term that does
* not yet exist. After processing the transformed row is passed to the
* destination phase where it is loaded (saved) into the Drupal 8 site.
*
* The ETL process is configured by the migration plugin. The different phases:
* source, process, and destination are also plugins, and are managed by the
* Migration plugin. So there are four types of plugins in the migration
* process: migration, source, process and destination.
*
* @section sec_migrations Migration plugins
* Migration plugin definitions are stored in a module's 'migrations' directory.
* For backwards compatibility we also scan the 'migration_templates' directory.
* Examples of migration plugin definitions can be found in
* 'core/modules/action/migration_templates'. The plugin class is
* \Drupal\migrate\Plugin\Migration, with interface
* \Drupal\migrate\Plugin\MigrationInterface. Migration plugins are managed by
* the \Drupal\migrate\Plugin\MigrationPluginManager class. Migration plugins
* are only available if the providers of their source plugins are installed.
*
* @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\MigrateSourcePluginManager class. Source plugin
* providers are determined by their and their parents namespaces.
*
* @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. The Migrate module
* provides process plugins for common operations (setting default values,
* mapping values, etc.).
*
* @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. The Migrate module provides destination plugins for Drupal core
* objects (configuration and entity).
*
* @section sec_more_info More information
* @link https://www.drupal.org/node/2127611 Migration API documentation. @endlink
*
* @see update_api
* @}
*/
/**
* @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));
}
}
}
/**
* Allows altering the list of discovered migration plugins.
*
* Modules are able to alter specific migrations structures or even remove or
* append additional migrations to the discovery. For example, this
* implementation filters out Drupal 6 migrations from the discovered migration
* list. This is done by checking the migration tags.
*
* @param array[] $migrations
* An associative array of migrations keyed by migration ID. Each value is the
* migration array, obtained by decoding the migration YAML file and enriched
* with some meta information added during discovery phase, like migration
* 'class', 'provider' or '_discovered_file_path'.
*
* @ingroup migration
*/
function hook_migration_plugins_alter(array &$migrations) {
$migrations = array_filter($migrations, function (array $migration) {
$tags = isset($migration['migration_tags']) ? (array) $migration['migration_tags'] : [];
return !in_array('Drupal 6', $tags);
});
}
/**
* @} End of "addtogroup hooks".
*/

View file

@ -0,0 +1,6 @@
name: Migrate
type: module
description: 'Handles migrations'
package: Core (Experimental)
version: VERSION
core: 8.x

View file

@ -0,0 +1,27 @@
<?php
/**
* @file
* Contains install and update functions for Migrate.
*/
/**
* @addtogroup updates-8.0.0-beta
* @{
*/
/**
* Remove load plugin references from existing migrations.
*/
function migrate_update_8001() {
$config_factory = \Drupal::configFactory();
foreach ($config_factory->listAll('migrate.migration.') as $migration_config_name) {
$migration = $config_factory->getEditable($migration_config_name);
$migration->clear('load');
$migration->save(TRUE);
}
}
/**
* @} End of "addtogroup updates-8.0.0-beta".
*/

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,26 @@
services:
migrate.plugin_event_subscriber:
class: Drupal\migrate\Plugin\PluginEventSubscriber
tags:
- { name: event_subscriber }
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\MigrateSourcePluginManager
arguments: [source, '@container.namespaces', '@cache.discovery', '@module_handler']
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']
plugin.manager.migration:
class: Drupal\migrate\Plugin\MigrationPluginManager
arguments: ['@module_handler', '@cache.discovery', '@language_manager']

View file

@ -0,0 +1,46 @@
<?php
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;
}

View file

@ -0,0 +1,48 @@
<?php
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,99 @@
<?php
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 implements MultipleProviderAnnotationInterface {
/**
* A unique identifier for the process plugin.
*
* @var string
*/
public $id;
/**
* Whether requirements are met.
*
* @var bool
*/
public $requirements_met = TRUE;
/**
* Identifies the system providing the data the source plugin will read.
*
* This can be any type, and the source plugin itself determines how the value
* is used. For example, Migrate Drupal's source plugins expect
* source_provider to be the name of a module that must be installed and
* enabled in the source database.
*
* @see \Drupal\migrate_drupal\Plugin\migrate\source\DrupalSqlBase::checkRequirements
*
* @var mixed
*/
public $source_provider;
/**
* Specifies the minimum version of the source provider.
*
* This can be any type, and the source plugin itself determines how it is
* used. For example, Migrate Drupal's source plugins expect this to be an
* integer representing the minimum installed database schema version of the
* module specified by source_provider.
*
* @var mixed
*/
public $minimum_version;
/**
* {@inheritdoc}
*/
public function getProvider() {
if (isset($this->definition['provider'])) {
return is_array($this->definition['provider']) ? reset($this->definition['provider']) : $this->definition['provider'];
}
return FALSE;
}
/**
* {@inheritdoc}
*/
public function getProviders() {
if (isset($this->definition['provider'])) {
// Ensure that we return an array even if
// \Drupal\Component\Annotation\AnnotationInterface::setProvider() has
// been called.
return (array) $this->definition['provider'];
}
return [];
}
/**
* {@inheritdoc}
*/
public function setProviders(array $providers) {
$this->definition['provider'] = $providers;
}
}

View file

@ -0,0 +1,41 @@
<?php
namespace Drupal\migrate\Annotation;
use Drupal\Component\Annotation\AnnotationInterface;
/**
* Defines a common interface for classed annotations with multiple providers.
*
* @todo This is a temporary solution to the fact that migration source plugins
* have more than one provider. This functionality will be moved to core in
* https://www.drupal.org/node/2786355.
*/
interface MultipleProviderAnnotationInterface extends AnnotationInterface {
/**
* Gets the name of the provider of the annotated class.
*
* @return string
* The provider of the annotation. If there are multiple providers the first
* is returned.
*/
public function getProvider();
/**
* Gets the provider names of the annotated class.
*
* @return string[]
* The providers of the annotation.
*/
public function getProviders();
/**
* Sets the provider names of the annotated class.
*
* @param string[] $providers
* The providers of the annotation.
*/
public function setProviders(array $providers);
}

View file

@ -0,0 +1,60 @@
<?php
namespace Drupal\migrate\Event;
use Drupal\migrate\Plugin\MigrationInterface;
use Drupal\migrate\MigrateMessageInterface;
use Symfony\Component\EventDispatcher\Event as SymfonyEvent;
class EventBase extends SymfonyEvent {
/**
* The migration.
*
* @var \Drupal\migrate\Plugin\MigrationInterface
*/
protected $migration;
/**
* The current message service.
*
* @var \Drupal\migrate\MigrateMessageInterface
*/
protected $message;
/**
* Constructs a Migrate event object.
*
* @param \Drupal\migrate\Plugin\MigrationInterface $migration
* The migration being run.
* @param \Drupal\migrate\MigrateMessageInterface $message
* The Migrate message service.
*/
public function __construct(MigrationInterface $migration, MigrateMessageInterface $message) {
$this->migration = $migration;
$this->message = $message;
}
/**
* Gets the migration.
*
* @return \Drupal\migrate\Plugin\MigrationInterface
* The migration being run.
*/
public function getMigration() {
return $this->migration;
}
/**
* Logs a message using the Migrate message service.
*
* @param string $message
* The message to log.
* @param string $type
* The type of message, for example: status or warning.
*/
public function logMessage($message, $type = 'status') {
$this->message->display($message, $type);
}
}

View file

@ -0,0 +1,26 @@
<?php
namespace Drupal\migrate\Event;
/**
* Interface for plugins that react to pre- or post-import events.
*/
interface ImportAwareInterface {
/**
* Performs pre-import tasks.
*
* @param \Drupal\migrate\Event\MigrateImportEvent $event
* The pre-import event object.
*/
public function preImport(MigrateImportEvent $event);
/**
* Performs post-import tasks.
*
* @param \Drupal\migrate\Event\MigrateImportEvent $event
* The post-import event object.
*/
public function postImport(MigrateImportEvent $event);
}

View file

@ -0,0 +1,184 @@
<?php
namespace Drupal\migrate\Event;
/**
* Defines events for the migration system.
*
* @see \Drupal\migrate\Event\MigrateMapSaveEvent
* @see \Drupal\migrate\Event\MigrateMapDeleteEvent
* @see \Drupal\migrate\Event\MigrateImportEvent
* @see \Drupal\migrate\Event\MigratePreRowSaveEvent
* @see \Drupal\migrate\Event\MigratePostRowSaveEvent
* @see \Drupal\migrate\Event\MigrateRollbackEvent
* @see \Drupal\migrate\Event\MigrateRowDeleteEvent
* @see \Drupal\migrate\Event\MigrateIdMapMessageEvent
*/
final class MigrateEvents {
/**
* Name of the event fired when saving to a migration's map.
*
* This event allows modules to perform an action whenever the disposition of
* an item being migrated is saved to the map table. The event listener method
* receives a \Drupal\migrate\Event\MigrateMapSaveEvent instance.
*
* @Event
*
* @see \Drupal\migrate\Event\MigrateMapSaveEvent
*
* @var string
*/
const MAP_SAVE = 'migrate.map_save';
/**
* Name of the event fired when removing an entry from a migration's map.
*
* This event allows modules to perform an action whenever a row is deleted
* from a migration's map table (implying it has been rolled back). The event
* listener method receives a \Drupal\migrate\Event\MigrateMapDeleteEvent instance.
*
* @Event
*
* @see \Drupal\migrate\Event\MigrateMapDeleteEvent
*
* @var string
*/
const MAP_DELETE = 'migrate.map_delete';
/**
* Name of the event fired when beginning a migration import operation.
*
* This event allows modules to perform an action whenever a migration import
* operation is about to begin. The event listener method receives a
* \Drupal\migrate\Event\MigrateImportEvent instance.
*
* @Event
*
* @see \Drupal\migrate\Event\MigrateImportEvent
*
* @var string
*/
const PRE_IMPORT = 'migrate.pre_import';
/**
* Name of the event fired when finishing a migration import operation.
*
* This event allows modules to perform an action whenever a migration import
* operation is completing. The event listener method receives a
* \Drupal\migrate\Event\MigrateImportEvent instance.
*
* @Event
*
* @see \Drupal\migrate\Event\MigrateImportEvent
*
* @var string
*/
const POST_IMPORT = 'migrate.post_import';
/**
* Name of the event fired when about to import a single item.
*
* This event allows modules to perform an action whenever a specific item
* is about to be saved by the destination plugin. The event listener method
* receives a \Drupal\migrate\Event\MigratePreRowSaveEvent instance.
*
* @Event
*
* @see \Drupal\migrate\Event\MigratePreRowSaveEvent
*
* @var string
*/
const PRE_ROW_SAVE = 'migrate.pre_row_save';
/**
* Name of the event fired just after a single item has been imported.
*
* This event allows modules to perform an action whenever a specific item
* has been saved by the destination plugin. The event listener method
* receives a \Drupal\migrate\Event\MigratePostRowSaveEvent instance.
*
* @Event
*
* @see \Drupal\migrate\Event\MigratePostRowSaveEvent
*
* @var string
*/
const POST_ROW_SAVE = 'migrate.post_row_save';
/**
* Name of the event fired when beginning a migration rollback operation.
*
* This event allows modules to perform an action whenever a migration
* rollback operation is about to begin. The event listener method receives a
* \Drupal\migrate\Event\MigrateRollbackEvent instance.
*
* @Event
*
* @see \Drupal\migrate\Event\MigrateRollbackEvent
*
* @var string
*/
const PRE_ROLLBACK = 'migrate.pre_rollback';
/**
* Name of the event fired when finishing a migration rollback operation.
*
* This event allows modules to perform an action whenever a migration
* rollback operation is completing. The event listener method receives a
* \Drupal\migrate\Event\MigrateRollbackEvent instance.
*
* @Event
*
* @see \Drupal\migrate\Event\MigrateRollbackEvent
*
* @var string
*/
const POST_ROLLBACK = 'migrate.post_rollback';
/**
* Name of the event fired when about to delete a single item.
*
* This event allows modules to perform an action whenever a specific item
* is about to be deleted by the destination plugin. The event listener method
* receives a \Drupal\migrate\Event\MigrateRowDeleteEvent instance.
*
* @Event
*
* @see \Drupal\migrate\Event\MigrateRowDeleteEvent
*
* @var string
*/
const PRE_ROW_DELETE = 'migrate.pre_row_delete';
/**
* Name of the event fired just after a single item has been deleted.
*
* This event allows modules to perform an action whenever a specific item
* has been deleted by the destination plugin. The event listener method
* receives a \Drupal\migrate\Event\MigrateRowDeleteEvent instance.
*
* @Event
*
* @see \Drupal\migrate\Event\MigrateRowDeleteEvent
*
* @var string
*/
const POST_ROW_DELETE = 'migrate.post_row_delete';
/**
* Name of the event fired when saving a message to the idmap.
*
* This event allows modules to perform an action whenever a message is being
* logged by the idmap. The event listener method
* receives a \Drupal\migrate\Event\MigrateIdMapMessageEvent instance.
*
* @Event
*
* @see \Drupal\migrate\Event\MigrateIdMapMessageEvent
*
* @var string
*/
const IDMAP_MESSAGE = 'migrate.idmap_message';
}

View file

@ -0,0 +1,101 @@
<?php
namespace Drupal\migrate\Event;
use Drupal\migrate\Plugin\MigrationInterface;
use Symfony\Component\EventDispatcher\Event;
/**
* Wraps an idmap message event for event listeners.
*/
class MigrateIdMapMessageEvent extends Event {
/**
* Migration entity.
*
* @var \Drupal\migrate\Plugin\MigrationInterface
*/
protected $migration;
/**
* Array of values uniquely identifying the source row.
*
* @var array
*/
protected $sourceIdValues;
/**
* Message to be logged.
*
* @var string
*/
protected $message;
/**
* Message severity.
*
* @var int
*/
protected $level;
/**
* Constructs a post-save event object.
*
* @param \Drupal\migrate\Plugin\MigrationInterface $migration
* Migration entity.
* @param array $source_id_values
* Values represent the source ID.
* @param string $message
* The message
* @param int $level
* Severity level (one of the MigrationInterface::MESSAGE_* constants).
*/
public function __construct(MigrationInterface $migration, array $source_id_values, $message, $level) {
$this->migration = $migration;
$this->sourceIdValues = $source_id_values;
$this->message = $message;
$this->level = $level;
}
/**
* Gets the migration entity.
*
* @return \Drupal\migrate\Plugin\MigrationInterface
* The migration entity involved.
*/
public function getMigration() {
return $this->migration;
}
/**
* Gets the source ID values.
*
* @return array
* The source ID as an array.
*/
public function getSourceIdValues() {
return $this->sourceIdValues;
}
/**
* Gets the message to be logged.
*
* @return string
* The message text.
*/
public function getMessage() {
return $this->message;
}
/**
* Gets the severity level of the message (one of the
* MigrationInterface::MESSAGE_* constants).
*
* @return int
* The message level.
*/
public function getLevel() {
return $this->level;
}
}

View file

@ -0,0 +1,8 @@
<?php
namespace Drupal\migrate\Event;
/**
* Wraps a pre- or post-import event for event listeners.
*/
class MigrateImportEvent extends EventBase {}

View file

@ -0,0 +1,60 @@
<?php
namespace Drupal\migrate\Event;
use Drupal\migrate\Plugin\MigrateIdMapInterface;
use Symfony\Component\EventDispatcher\Event;
/**
* Wraps a migrate map delete event for event listeners.
*/
class MigrateMapDeleteEvent extends Event {
/**
* Map plugin.
*
* @var \Drupal\migrate\Plugin\MigrateIdMapInterface
*/
protected $map;
/**
* Array of source ID fields.
*
* @var array
*/
protected $sourceId;
/**
* Constructs a migration map delete event object.
*
* @param \Drupal\migrate\Plugin\MigrateIdMapInterface $map
* Map plugin.
* @param array $source_id
* Array of source ID fields representing the object being deleted from the map.
*/
public function __construct(MigrateIdMapInterface $map, array $source_id) {
$this->map = $map;
$this->sourceId = $source_id;
}
/**
* Gets the map plugin.
*
* @return \Drupal\migrate\Plugin\MigrateIdMapInterface
* The map plugin that caused the event to fire.
*/
public function getMap() {
return $this->map;
}
/**
* Gets the source ID of the item being removed from the map.
*
* @return array
* Array of source ID fields.
*/
public function getSourceId() {
return $this->sourceId;
}
}

View file

@ -0,0 +1,60 @@
<?php
namespace Drupal\migrate\Event;
use Drupal\migrate\Plugin\MigrateIdMapInterface;
use Symfony\Component\EventDispatcher\Event;
/**
* Wraps a migrate map save event for event listeners.
*/
class MigrateMapSaveEvent extends Event {
/**
* Map plugin.
*
* @var \Drupal\migrate\Plugin\MigrateIdMapInterface
*/
protected $map;
/**
* Array of fields being saved to the map, keyed by field name.
*
* @var array
*/
protected $fields;
/**
* Constructs a migration map event object.
*
* @param \Drupal\migrate\Plugin\MigrateIdMapInterface $map
* Map plugin.
* @param array $fields
* Array of fields being saved to the map.
*/
public function __construct(MigrateIdMapInterface $map, array $fields) {
$this->map = $map;
$this->fields = $fields;
}
/**
* Gets the map plugin.
*
* @return \Drupal\migrate\Plugin\MigrateIdMapInterface
* The map plugin that caused the event to fire.
*/
public function getMap() {
return $this->map;
}
/**
* Gets the fields about to be saved to the map.
*
* @return array
* Array of map fields, keyed by field name.
*/
public function getFields() {
return $this->fields;
}
}

View file

@ -0,0 +1,48 @@
<?php
namespace Drupal\migrate\Event;
use Drupal\migrate\Plugin\MigrationInterface;
use Drupal\migrate\MigrateMessageInterface;
use Drupal\migrate\Row;
/**
* Wraps a post-save event for event listeners.
*/
class MigratePostRowSaveEvent extends MigratePreRowSaveEvent {
/**
* The row's destination ID.
*
* @var array|bool
*/
protected $destinationIdValues = [];
/**
* Constructs a post-save event object.
*
* @param \Drupal\migrate\Plugin\MigrationInterface $migration
* Migration entity.
* @param \Drupal\migrate\MigrateMessageInterface $message
* The message interface.
* @param \Drupal\migrate\Row $row
* Row object.
* @param array|bool $destination_id_values
* Values represent the destination ID.
*/
public function __construct(MigrationInterface $migration, MigrateMessageInterface $message, Row $row, $destination_id_values) {
parent::__construct($migration, $message, $row);
$this->destinationIdValues = $destination_id_values;
}
/**
* Gets the destination ID values.
*
* @return array
* The destination ID as an array.
*/
public function getDestinationIdValues() {
return $this->destinationIdValues;
}
}

View file

@ -0,0 +1,45 @@
<?php
namespace Drupal\migrate\Event;
use Drupal\migrate\Plugin\MigrationInterface;
use Drupal\migrate\MigrateMessageInterface;
use Drupal\migrate\Row;
/**
* Wraps a pre-save event for event listeners.
*/
class MigratePreRowSaveEvent extends EventBase {
/**
* Row object.
*
* @var \Drupal\migrate\Row
*/
protected $row;
/**
* Constructs a pre-save event object.
*
* @param \Drupal\migrate\Plugin\MigrationInterface $migration
* Migration entity.
* @param \Drupal\migrate\MigrateMessageInterface $message
* The current migrate message service.
* @param \Drupal\migrate\Row $row
*/
public function __construct(MigrationInterface $migration, MigrateMessageInterface $message, Row $row) {
parent::__construct($migration, $message);
$this->row = $row;
}
/**
* Gets the row object.
*
* @return \Drupal\migrate\Row
* The row object about to be imported.
*/
public function getRow() {
return $this->row;
}
}

View file

@ -0,0 +1,40 @@
<?php
namespace Drupal\migrate\Event;
use Drupal\migrate\Plugin\MigrationInterface;
use Symfony\Component\EventDispatcher\Event;
/**
* Wraps a pre- or post-rollback event for event listeners.
*/
class MigrateRollbackEvent extends Event {
/**
* Migration entity.
*
* @var \Drupal\migrate\Plugin\MigrationInterface
*/
protected $migration;
/**
* Constructs an rollback event object.
*
* @param \Drupal\migrate\Plugin\MigrationInterface $migration
* Migration entity.
*/
public function __construct(MigrationInterface $migration) {
$this->migration = $migration;
}
/**
* Gets the migration entity.
*
* @return \Drupal\migrate\Plugin\MigrationInterface
* The migration entity involved.
*/
public function getMigration() {
return $this->migration;
}
}

View file

@ -0,0 +1,60 @@
<?php
namespace Drupal\migrate\Event;
use Drupal\migrate\Plugin\MigrationInterface;
use Symfony\Component\EventDispatcher\Event;
/**
* Wraps a row deletion event for event listeners.
*/
class MigrateRowDeleteEvent extends Event {
/**
* Migration entity.
*
* @var \Drupal\migrate\Plugin\MigrationInterface
*/
protected $migration;
/**
* Values representing the destination ID.
*
* @var array
*/
protected $destinationIdValues;
/**
* Constructs a row deletion event object.
*
* @param \Drupal\migrate\Plugin\MigrationInterface $migration
* Migration entity.
* @param array $destination_id_values
* Values represent the destination ID.
*/
public function __construct(MigrationInterface $migration, $destination_id_values) {
$this->migration = $migration;
$this->destinationIdValues = $destination_id_values;
}
/**
* Gets the migration entity.
*
* @return \Drupal\migrate\Plugin\MigrationInterface
* The migration being rolled back.
*/
public function getMigration() {
return $this->migration;
}
/**
* Gets the destination ID values.
*
* @return array
* The destination ID as an array.
*/
public function getDestinationIdValues() {
return $this->destinationIdValues;
}
}

View file

@ -0,0 +1,26 @@
<?php
namespace Drupal\migrate\Event;
/**
* Interface for plugins that react to pre- or post-rollback events.
*/
interface RollbackAwareInterface {
/**
* Performs pre-rollback tasks.
*
* @param \Drupal\migrate\Event\MigrateRollbackEvent $event
* The pre-rollback event object.
*/
public function preRollback(MigrateRollbackEvent $event);
/**
* Performs post-rollback tasks.
*
* @param \Drupal\migrate\Event\MigrateRollbackEvent $event
* The post-rollback event object.
*/
public function postRollback(MigrateRollbackEvent $event);
}

View file

@ -0,0 +1,69 @@
<?php
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) {
if (!is_array($requirements)) {
$requirements = array($requirements);
}
foreach ($requirements as $value) {
$output .= "$requirement_type: $value. ";
}
}
return trim($output);
}
}

View file

@ -0,0 +1,22 @@
<?php
namespace Drupal\migrate;
interface MigrateBuildDependencyInterface {
/**
* Builds a dependency tree for the migrations and set their order.
*
* @param \Drupal\migrate\Plugin\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,72 @@
<?php
namespace Drupal\migrate;
use Drupal\migrate\Plugin\MigrateIdMapInterface;
use Drupal\migrate\Plugin\MigrationInterface;
/**
* 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,553 @@
<?php
namespace Drupal\migrate;
use Drupal\Component\Utility\Bytes;
use Drupal\Core\Utility\Error;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Drupal\migrate\Event\MigrateEvents;
use Drupal\migrate\Event\MigrateImportEvent;
use Drupal\migrate\Event\MigratePostRowSaveEvent;
use Drupal\migrate\Event\MigratePreRowSaveEvent;
use Drupal\migrate\Event\MigrateRollbackEvent;
use Drupal\migrate\Event\MigrateRowDeleteEvent;
use Drupal\migrate\Exception\RequirementsException;
use Drupal\migrate\Plugin\MigrateIdMapInterface;
use Drupal\migrate\Plugin\MigrationInterface;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
/**
* Defines a migrate executable class.
*/
class MigrateExecutable implements MigrateExecutableInterface {
use StringTranslationTrait;
/**
* The configuration of the migration to do.
*
* @var \Drupal\migrate\Plugin\MigrationInterface
*/
protected $migration;
/**
* Status of one row.
*
* The value is a MigrateIdMapInterface::STATUS_* constant, for example:
* STATUS_IMPORTED.
*
* @var int
*/
protected $sourceRowStatus;
/**
* The ratio of the memory limit at which an operation will be interrupted.
*
* @var float
*/
protected $memoryThreshold = 0.85;
/**
* The PHP memory_limit expressed in bytes.
*
* @var int
*/
protected $memoryLimit;
/**
* The configuration values of the source.
*
* @var array
*/
protected $sourceIdValues;
/**
* An array of counts. Initially used for cache hit/miss tracking.
*
* @var array
*/
protected $counts = array();
/**
* The source.
*
* @var \Drupal\migrate\Plugin\MigrateSourceInterface
*/
protected $source;
/**
* The event dispatcher.
*
* @var \Symfony\Component\EventDispatcher\EventDispatcherInterface
*/
protected $eventDispatcher;
/**
* Migration message service.
*
* @todo https://www.drupal.org/node/2822663 Make this protected.
*
* @var \Drupal\migrate\MigrateMessageInterface
*/
public $message;
/**
* Constructs a MigrateExecutable and verifies and sets the memory limit.
*
* @param \Drupal\migrate\Plugin\MigrationInterface $migration
* The migration to run.
* @param \Drupal\migrate\MigrateMessageInterface $message
* The message to record.
* @param \Symfony\Component\EventDispatcher\EventDispatcherInterface $event_dispatcher
* The event dispatcher.
*
* @throws \Drupal\migrate\MigrateException
*/
public function __construct(MigrationInterface $migration, MigrateMessageInterface $message, EventDispatcherInterface $event_dispatcher = NULL) {
$this->migration = $migration;
$this->message = $message;
$this->migration->getIdMap()->setMessage($message);
$this->eventDispatcher = $event_dispatcher;
// Record the memory limit in bytes
$limit = trim(ini_get('memory_limit'));
if ($limit == '-1') {
$this->memoryLimit = PHP_INT_MAX;
}
else {
$this->memoryLimit = Bytes::toInt($limit);
}
}
/**
* 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();
}
return $this->source;
}
/**
* Gets the event dispatcher.
*
* @return \Symfony\Component\EventDispatcher\EventDispatcherInterface
*/
protected function getEventDispatcher() {
if (!$this->eventDispatcher) {
$this->eventDispatcher = \Drupal::service('event_dispatcher');
}
return $this->eventDispatcher;
}
/**
* {@inheritdoc}
*/
public function import() {
// Only begin the import operation if the migration is currently idle.
if ($this->migration->getStatus() !== MigrationInterface::STATUS_IDLE) {
$this->message->display($this->t('Migration @id is busy with another operation: @status',
array(
'@id' => $this->migration->id(),
'@status' => $this->t($this->migration->getStatusLabel()),
)), 'error');
return MigrationInterface::RESULT_FAILED;
}
$this->getEventDispatcher()->dispatch(MigrateEvents::PRE_IMPORT, new MigrateImportEvent($this->migration, $this->message));
// 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;
}
$this->migration->setStatus(MigrationInterface::STATUS_IMPORTING);
$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');
$this->migration->setStatus(MigrationInterface::STATUS_IDLE);
return MigrationInterface::RESULT_FAILED;
}
$destination = $this->migration->getDestinationPlugin();
while ($source->valid()) {
$row = $source->current();
$this->sourceIdValues = $row->getSourceIdValues();
try {
$this->processRow($row);
$save = TRUE;
}
catch (MigrateException $e) {
$this->migration->getIdMap()->saveIdMapping($row, array(), $e->getStatus());
$this->saveMessage($e->getMessage(), $e->getLevel());
$save = FALSE;
}
catch (MigrateSkipRowException $e) {
if ($e->getSaveToMap()) {
$id_map->saveIdMapping($row, [], MigrateIdMapInterface::STATUS_IGNORED);
}
if ($message = trim($e->getMessage())) {
$this->saveMessage($message, MigrationInterface::MESSAGE_INFORMATIONAL);
}
$save = FALSE;
}
if ($save) {
try {
$this->getEventDispatcher()->dispatch(MigrateEvents::PRE_ROW_SAVE, new MigratePreRowSaveEvent($this->migration, $this->message, $row));
$destination_id_values = $destination->import($row, $id_map->lookupDestinationId($this->sourceIdValues));
$this->getEventDispatcher()->dispatch(MigrateEvents::POST_ROW_SAVE, new MigratePostRowSaveEvent($this->migration, $this->message, $row, $destination_id_values));
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, $destination->rollbackAction());
}
}
else {
$id_map->saveIdMapping($row, array(), MigrateIdMapInterface::STATUS_FAILED);
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->saveMessage($e->getMessage(), $e->getLevel());
}
catch (\Exception $e) {
$this->migration->getIdMap()->saveIdMapping($row, array(), MigrateIdMapInterface::STATUS_FAILED);
$this->handleException($e);
}
}
$this->sourceRowStatus = MigrateIdMapInterface::STATUS_IMPORTED;
// Check for memory exhaustion.
if (($return = $this->checkStatus()) != MigrationInterface::RESULT_COMPLETED) {
break;
}
// If anyone has requested we stop, return the requested result.
if ($this->migration->getStatus() == MigrationInterface::STATUS_STOPPING) {
$return = $this->migration->getInterruptionResult();
$this->migration->clearInterruptionResult();
break;
}
try {
$source->next();
}
catch (\Exception $e) {
$this->message->display(
$this->t('Migration failed with source plugin exception: @e',
array('@e' => $e->getMessage())), 'error');
$this->migration->setStatus(MigrationInterface::STATUS_IDLE);
return MigrationInterface::RESULT_FAILED;
}
}
$this->getEventDispatcher()->dispatch(MigrateEvents::POST_IMPORT, new MigrateImportEvent($this->migration, $this->message));
$this->migration->setStatus(MigrationInterface::STATUS_IDLE);
return $return;
}
/**
* {@inheritdoc}
*/
public function rollback() {
// Only begin the rollback operation if the migration is currently idle.
if ($this->migration->getStatus() !== MigrationInterface::STATUS_IDLE) {
$this->message->display($this->t('Migration @id is busy with another operation: @status', ['@id' => $this->migration->id(), '@status' => $this->t($this->migration->getStatusLabel())]), 'error');
return MigrationInterface::RESULT_FAILED;
}
// Announce that rollback is about to happen.
$this->getEventDispatcher()->dispatch(MigrateEvents::PRE_ROLLBACK, new MigrateRollbackEvent($this->migration));
// Optimistically assume things are going to work out; if not, $return will be
// updated to some other status.
$return = MigrationInterface::RESULT_COMPLETED;
$this->migration->setStatus(MigrationInterface::STATUS_ROLLING_BACK);
$id_map = $this->migration->getIdMap();
$destination = $this->migration->getDestinationPlugin();
// Loop through each row in the map, and try to roll it back.
foreach ($id_map as $map_row) {
$destination_key = $id_map->currentDestination();
if ($destination_key) {
$map_row = $id_map->getRowByDestination($destination_key);
if ($map_row['rollback_action'] == MigrateIdMapInterface::ROLLBACK_DELETE) {
$this->getEventDispatcher()
->dispatch(MigrateEvents::PRE_ROW_DELETE, new MigrateRowDeleteEvent($this->migration, $destination_key));
$destination->rollback($destination_key);
$this->getEventDispatcher()
->dispatch(MigrateEvents::POST_ROW_DELETE, new MigrateRowDeleteEvent($this->migration, $destination_key));
}
// We're now done with this row, so remove it from the map.
$id_map->deleteDestination($destination_key);
}
else {
// If there is no destination key the import probably failed and we can
// remove the row without further action.
$source_key = $id_map->currentSource();
$id_map->delete($source_key);
}
// Check for memory exhaustion.
if (($return = $this->checkStatus()) != MigrationInterface::RESULT_COMPLETED) {
break;
}
// If anyone has requested we stop, return the requested result.
if ($this->migration->getStatus() == MigrationInterface::STATUS_STOPPING) {
$return = $this->migration->getInterruptionResult();
$this->migration->clearInterruptionResult();
break;
}
}
// Notify modules that rollback attempt was complete.
$this->getEventDispatcher()->dispatch(MigrateEvents::POST_ROLLBACK, new MigrateRollbackEvent($this->migration));
$this->migration->setStatus(MigrationInterface::STATUS_IDLE);
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) {
$new_value[] = NULL;
$break = TRUE;
}
}
$value = $new_value;
if ($break) {
break;
}
}
else {
try {
$value = $plugin->transform($value, $this, $row, $destination);
}
catch (MigrateSkipProcessException $e) {
$value = NULL;
break;
}
$multiple = $plugin->multiple();
}
}
// No plugins or no value means do not set.
if ($plugins && !is_null($value)) {
$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();
}
/**
* {@inheritdoc}
*/
public function saveMessage($message, $level = MigrationInterface::MESSAGE_ERROR) {
$this->migration->getIdMap()->saveMessage($this->sourceIdValues, $message, $level);
}
/**
* 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');
}
/**
* Checks for exceptional conditions, and display feedback.
*/
protected function checkStatus() {
if ($this->memoryExceeded()) {
return 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();
// Entity storage can blow up with caches so clear them out.
$manager = \Drupal::entityManager();
foreach ($manager->getDefinitions() as $id => $definition) {
$manager->getStorage($id)->resetCache();
}
// @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);
}
}

View file

@ -0,0 +1,49 @@
<?php
namespace Drupal\migrate;
use Drupal\migrate\Plugin\MigrationInterface;
interface MigrateExecutableInterface {
/**
* Performs an import operation - migrate items from source to destination.
*/
public function import();
/**
* Performs a rollback operation - remove previously-imported items.
*/
public function rollback();
/**
* 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);
/**
* 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);
}

View file

@ -0,0 +1,30 @@
<?php
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,18 @@
<?php
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,10 @@
<?php
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,41 @@
<?php
namespace Drupal\migrate;
/**
* This exception is thrown when a row should be skipped.
*/
class MigrateSkipRowException extends \Exception {
/**
* Whether to record the skip in the map table, or skip silently.
*
* @var bool
* TRUE to record as STATUS_IGNORED in the map, FALSE to skip silently.
*/
protected $saveToMap;
/**
* Constructs a MigrateSkipRowException object.
*
* @param string $message
* The message for the exception.
* @param bool $save_to_map
* TRUE to record as STATUS_IGNORED in the map, FALSE to skip silently.
*/
public function __construct($message = NULL, $save_to_map = TRUE) {
parent::__construct($message);
$this->saveToMap = $save_to_map;
}
/**
* Whether the thrower wants to record this skip in the map table.
*
* @return bool
* TRUE to record as STATUS_IGNORED in the map, FALSE to skip silently.
*/
public function getSaveToMap() {
return $this->saveToMap;
}
}

View file

@ -0,0 +1,72 @@
<?php
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,71 @@
<?php
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,138 @@
<?php
namespace Drupal\migrate\Plugin\Discovery;
use Doctrine\Common\Annotations\AnnotationRegistry;
use Doctrine\Common\Reflection\StaticReflectionParser as BaseStaticReflectionParser;
use Drupal\Component\Annotation\AnnotationInterface;
use Drupal\Component\Annotation\Reflection\MockFileFinder;
use Drupal\Component\ClassFinder\ClassFinder;
use Drupal\Core\Plugin\Discovery\AnnotatedClassDiscovery;
use Drupal\migrate\Annotation\MultipleProviderAnnotationInterface;
/**
* Determines providers based on a class's and its parent's namespaces.
*
* @internal
* This is a temporary solution to the fact that migration source plugins have
* more than one provider. This functionality will be moved to core in
* https://www.drupal.org/node/2786355.
*/
class AnnotatedClassDiscoveryAutomatedProviders extends AnnotatedClassDiscovery {
/**
* A utility object that can use active autoloaders to find files for classes.
*
* @var \Doctrine\Common\Reflection\ClassFinderInterface
*/
protected $finder;
/**
* Constructs an AnnotatedClassDiscoveryAutomatedProviders object.
*
* @param string $subdir
* Either the plugin's subdirectory, for example 'Plugin/views/filter', or
* empty string if plugins are located at the top level of the namespace.
* @param \Traversable $root_namespaces
* An object that implements \Traversable which contains the root paths
* keyed by the corresponding namespace to look for plugin implementations.
* If $subdir is not an empty string, it will be appended to each namespace.
* @param string $plugin_definition_annotation_name
* The name of the annotation that contains the plugin definition.
* Defaults to 'Drupal\Component\Annotation\Plugin'.
* @param string[] $annotation_namespaces
* Additional namespaces to scan for annotation definitions.
*/
public function __construct($subdir, \Traversable $root_namespaces, $plugin_definition_annotation_name = 'Drupal\Component\Annotation\Plugin', array $annotation_namespaces = []) {
parent::__construct($subdir, $root_namespaces, $plugin_definition_annotation_name, $annotation_namespaces);
$this->finder = new ClassFinder();
}
/**
* {@inheritdoc}
*/
protected function prepareAnnotationDefinition(AnnotationInterface $annotation, $class, BaseStaticReflectionParser $parser = NULL) {
if (!($annotation instanceof MultipleProviderAnnotationInterface)) {
throw new \LogicException('AnnotatedClassDiscoveryAutomatedProviders annotations must implement \Drupal\migrate\Annotation\MultipleProviderAnnotationInterface');
}
$annotation->setClass($class);
$providers = $annotation->getProviders();
// Loop through all the parent classes and add their providers (which we
// infer by parsing their namespaces) to the $providers array.
do {
$providers[] = $this->getProviderFromNamespace($parser->getNamespaceName());
} while ($parser = StaticReflectionParser::getParentParser($parser, $this->finder));
$providers = array_unique(array_filter($providers, function ($provider) {
return $provider && $provider !== 'component';
}));
$annotation->setProviders($providers);
}
/**
* {@inheritdoc}
*/
public function getDefinitions() {
$definitions = array();
$reader = $this->getAnnotationReader();
// Clear the annotation loaders of any previous annotation classes.
AnnotationRegistry::reset();
// Register the namespaces of classes that can be used for annotations.
AnnotationRegistry::registerLoader('class_exists');
// Search for classes within all PSR-0 namespace locations.
foreach ($this->getPluginNamespaces() as $namespace => $dirs) {
foreach ($dirs as $dir) {
if (file_exists($dir)) {
$iterator = new \RecursiveIteratorIterator(
new \RecursiveDirectoryIterator($dir, \RecursiveDirectoryIterator::SKIP_DOTS)
);
foreach ($iterator as $fileinfo) {
if ($fileinfo->getExtension() == 'php') {
if ($cached = $this->fileCache->get($fileinfo->getPathName())) {
if (isset($cached['id'])) {
// Explicitly unserialize this to create a new object instance.
$definitions[$cached['id']] = unserialize($cached['content']);
}
continue;
}
$sub_path = $iterator->getSubIterator()->getSubPath();
$sub_path = $sub_path ? str_replace(DIRECTORY_SEPARATOR, '\\', $sub_path) . '\\' : '';
$class = $namespace . '\\' . $sub_path . $fileinfo->getBasename('.php');
// The filename is already known, so there is no need to find the
// file. However, StaticReflectionParser needs a finder, so use a
// mock version.
$finder = MockFileFinder::create($fileinfo->getPathName());
$parser = new BaseStaticReflectionParser($class, $finder, FALSE);
/** @var $annotation \Drupal\Component\Annotation\AnnotationInterface */
if ($annotation = $reader->getClassAnnotation($parser->getReflectionClass(), $this->pluginDefinitionAnnotationName)) {
$this->prepareAnnotationDefinition($annotation, $class, $parser);
$id = $annotation->getId();
$content = $annotation->get();
$definitions[$id] = $content;
// Explicitly serialize this to create a new object instance.
$this->fileCache->set($fileinfo->getPathName(), ['id' => $id, 'content' => serialize($content)]);
}
else {
// Store a NULL object, so the file is not reparsed again.
$this->fileCache->set($fileinfo->getPathName(), [NULL]);
}
}
}
}
}
}
// Don't let annotation loaders pile up.
AnnotationRegistry::reset();
return $definitions;
}
}

View file

@ -0,0 +1,99 @@
<?php
namespace Drupal\migrate\Plugin\Discovery;
use Drupal\Component\Plugin\Discovery\DiscoveryInterface;
use Drupal\Component\Plugin\Discovery\DiscoveryTrait;
/**
* Remove plugin definitions with non-existing providers.
*
* @internal
* This is a temporary solution to the fact that migration source plugins have
* more than one provider. This functionality will be moved to core in
* https://www.drupal.org/node/2786355.
*/
class ProviderFilterDecorator implements DiscoveryInterface {
use DiscoveryTrait;
/**
* The Discovery object being decorated.
*
* @var \Drupal\Component\Plugin\Discovery\DiscoveryInterface
*/
protected $decorated;
/**
* A callable for testing if a provider exists.
*
* @var callable
*/
protected $providerExists;
/**
* Constructs a InheritProviderDecorator object.
*
* @param \Drupal\Component\Plugin\Discovery\DiscoveryInterface $decorated
* The object implementing DiscoveryInterface that is being decorated.
* @param callable $provider_exists
* A callable, gets passed a provider name, should return TRUE if the
* provider exists and FALSE if not.
*/
public function __construct(DiscoveryInterface $decorated, callable $provider_exists) {
$this->decorated = $decorated;
$this->providerExists = $provider_exists;
}
/**
* Removes plugin definitions with non-existing providers.
*
* @param mixed[] $definitions
* An array of plugin definitions (empty array if no definitions were
* found). Keys are plugin IDs.
* @param callable $provider_exists
* A callable, gets passed a provider name, should return TRUE if the
* provider exists and FALSE if not.
*
* @return array|\mixed[] $definitions
* An array of plugin definitions. If a definition is an array and has a
* provider key that provider is guaranteed to exist.
*/
public static function filterDefinitions(array $definitions, callable $provider_exists) {
// Besides what the caller accepts, we also accept core or component.
$provider_exists = function ($provider) use ($provider_exists) {
return in_array($provider, ['core', 'component']) || $provider_exists($provider);
};
return array_filter($definitions, function ($definition) use ($provider_exists) {
// Plugin definitions can be objects (for example, Typed Data) those will
// become empty array here and cause no problems.
$definition = (array) $definition + ['provider' => []];
// There can be one or many providers, handle them as multiple always.
$providers = (array) $definition['provider'];
return count($providers) == count(array_filter($providers, $provider_exists));
});
}
/**
* {@inheritdoc}
*/
public function getDefinitions() {
return static::filterDefinitions($this->decorated->getDefinitions(), $this->providerExists);
}
/**
* Passes through all unknown calls onto the decorated object.
*
* @param string $method
* The method to call on the decorated object.
* @param array $args
* Call arguments.
*
* @return mixed
* The return value from the method on the decorated object.
*/
public function __call($method, array $args) {
return call_user_func_array([$this->decorated, $method], $args);
}
}

View file

@ -0,0 +1,40 @@
<?php
namespace Drupal\migrate\Plugin\Discovery;
use Doctrine\Common\Reflection\StaticReflectionParser as BaseStaticReflectionParser;
/**
* Allows getting the reflection parser for the parent class.
*
* @internal
* This is a temporary solution to the fact that migration source plugins have
* more than one provider. This functionality will be moved to core in
* https://www.drupal.org/node/2786355.
*/
class StaticReflectionParser extends BaseStaticReflectionParser {
/**
* If the current class extends another, get the parser for the latter.
*
* @param \Doctrine\Common\Reflection\StaticReflectionParser $parser
* The current static parser.
* @param $finder
* The class finder. Must implement
* \Doctrine\Common\Reflection\ClassFinderInterface, but can do so
* implicitly (i.e., implements the interface's methods but not the actual
* interface).
*
* @return static|null
* The static parser for the parent if there's a parent class or NULL.
*/
public static function getParentParser(BaseStaticReflectionParser $parser, $finder) {
// Ensure the class has been parsed before accessing the parentClassName
// property.
$parser->parse();
if ($parser->parentClassName) {
return new static($parser->parentClassName, $finder, $parser->classAnnotationOptimize);
}
}
}

View file

@ -0,0 +1,136 @@
<?php
namespace Drupal\migrate\Plugin;
use Drupal\Component\Plugin\PluginInspectionInterface;
use Drupal\migrate\Row;
/**
* Defines an interface for Migration Destination classes.
*
* 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 {
/**
* Gets the destination IDs.
*
* To support MigrateIdMap maps, derived destination classes should return
* 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 associative array of field definitions keyed by field ID. Values are
* associative arrays with a structure that contains the field type ('type'
* key). The other keys are the field storage settings as they are returned
* by FieldStorageDefinitionInterface::getSettings(). As an example, for a
* composite destination primary key that is defined by an integer and a
* string, the returned value might look like:
* @code
* return [
* 'id' => [
* 'type' => 'integer',
* 'unsigned' => FALSE,
* 'size' => 'big',
* ],
* 'version' => [
* 'type' => 'string',
* 'max_length' => 64,
* 'is_ascii' => TRUE,
* ],
* ];
* @endcode
* If 'type' points to a field plugin with multiple columns and needs to
* refer to a column different than 'value', the key of that column will be
* appended as a suffix to the plugin name, separated by dot ('.'). Example:
* @code
* return [
* 'format' => [
* 'type' => 'text.format',
* ],
* ];
* @endcode
* Additional custom keys/values, that are not part of field storage
* definition, can be passed in definitions:
* @code
* return [
* 'nid' => [
* 'type' => 'integer',
* 'custom_setting' => 'some_value',
* ],
* ];
* @endcode
*
* @see \Drupal\Core\Field\FieldStorageDefinitionInterface::getSettings()
* @see \Drupal\Core\Field\Plugin\Field\FieldType\IntegerItem
* @see \Drupal\Core\Field\Plugin\Field\FieldType\StringItem
* @see \Drupal\text\Plugin\Field\FieldType\TextItem
*/
public function getIds();
/**
* Returns an array of destination fields.
*
* Derived classes must implement fields(), returning a list of available
* destination fields.
*
* @param \Drupal\migrate\Plugin\MigrationInterface $migration
* Unused, will be removed before Drupal 9.0.x. Defaults to NULL.
*
* @return array
* - Keys: machine names of the fields
* - Values: Human-friendly descriptions of the fields.
*/
public function fields(MigrationInterface $migration = NULL);
/**
* 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
* (optional) The old destination IDs. Defaults to an empty array.
*
* @return mixed
* The entity ID or an indication of success.
*/
public function import(Row $row, array $old_destination_id_values = array());
/**
* Delete the specified destination object from the target Drupal.
*
* @param array $destination_identifier
* The ID of the destination object to delete.
*/
public function rollback(array $destination_identifier);
/**
* Whether the destination can be rolled back or not.
*
* @return bool
* TRUE if rollback is supported, FALSE if not.
*/
public function supportsRollback();
/**
* The rollback action for the last imported item.
*
* @return int
* The MigrateIdMapInterface::ROLLBACK_ constant indicating how an imported
* item should be handled on rollback.
*/
public function rollbackAction();
}

View file

@ -0,0 +1,64 @@
<?php
namespace Drupal\migrate\Plugin;
use Drupal\Core\Cache\CacheBackendInterface;
use Drupal\Core\Entity\EntityManagerInterface;
use Drupal\Core\Extension\ModuleHandlerInterface;
/**
* 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 entity manager.
*
* @var \Drupal\Core\Entity\EntityManagerInterface
*/
protected $entityManager;
/**
* Constructs a MigrateDestinationPluginManager 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 \Drupal\Core\Entity\EntityManagerInterface $entity_manager
* The entity manager.
* @param string $annotation
* (optional) The annotation class name. Defaults to
* 'Drupal\migrate\Annotation\MigrateDestination'.
*/
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,284 @@
<?php
namespace Drupal\migrate\Plugin;
use Drupal\Component\Plugin\PluginInspectionInterface;
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
* (optional) Status of the source row in the map. Defaults to
* self::STATUS_IMPORTED.
* @param int $rollback_action
* (optional) How to handle the destination object on rollback. Defaults to
* self::ROLLBACK_DELETE.
*/
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 keyed values of the record, e.g. ['nid' => 5].
* @param string $message
* The message to record.
* @param int $level
* (optional) The message severity. Defaults to
* MigrationInterface::MESSAGE_ERROR.
*/
public function saveMessage(array $source_id_values, $message, $level = MigrationInterface::MESSAGE_ERROR);
/**
* Retrieves an iterator over messages relate to source records.
*
* @param array $source_id_values
* (optional) The source identifier keyed values of the record, e.g.
* ['nid' => 5]. If empty (the default), all messages are retrieved.
* @param int $level
* (optional) Message severity. If NULL (the default), retrieve messages of
* all severities.
*
* @return \Iterator
* Retrieves an iterator over the message rows.
*/
public function getMessageIterator(array $source_id_values = [], $level = NULL);
/**
* 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 keyed values of the record, e.g. ['nid' => 5].
* @param bool $messages_only
* (optional) TRUE to only delete the migrate messages. Defaults to FALSE.
*/
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 key value pairs we should do the deletes for.
*/
public function deleteDestination(array $destination_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 keyed values of the record, e.g. ['nid' => 5].
*
* @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 keyed values of the record, e.g. ['nid' => 5].
*
* @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 keyed values of the record, e.g. ['nid' => 5].
*
* @return array
* The source identifier keyed values of the record, e.g. ['nid' => 5], or
* an empty array on failure.
*/
public function lookupSourceID(array $destination_id_values);
/**
* Looks up the destination identifier corresponding to a source key.
*
* 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 keyed values of the record, e.g. ['nid' => 5].
*
* @return array
* The destination identifier values of the record, or empty on failure.
*
* @deprecated in Drupal 8.1.x, will be removed before Drupal 9.0.x. Use
* lookupDestinationIds() instead.
*/
public function lookupDestinationId(array $source_id_values);
/**
* Looks up the destination identifiers corresponding to a source key.
*
* This can look up a subset of source keys if only some are provided, and
* will return all destination keys that match.
*
* @param array $source_id_values
* The source identifier keyed values of the records, e.g. ['nid' => 5].
* If unkeyed, the first count($source_id_values) keys will be assumed.
*
* @return array
* An array of arrays of destination identifier values.
*
* @throws \Drupal\migrate\MigrateException
* Thrown when $source_id_values contains unknown keys, or is the wrong
* length.
*/
public function lookupDestinationIds(array $source_id_values);
/**
* Looks up the destination identifier currently being iterated.
*
* @return array
* The destination identifier values of the record, or NULL on failure.
*/
public function currentDestination();
/**
* Looks up the source identifier(s) currently being iterated.
*
* @return array
* The source identifier values of the record, or NULL on failure.
*/
public function currentSource();
/**
* 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,65 @@
<?php
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;
/**
* 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 implements MigratePluginManagerInterface {
/**
* 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
* (optional) The annotation class name. Defaults to
* 'Drupal\Component\Annotation\PluginID'.
*/
public function __construct($type, \Traversable $namespaces, CacheBackendInterface $cache_backend, ModuleHandlerInterface $module_handler, $annotation = 'Drupal\Component\Annotation\PluginID') {
parent::__construct("Plugin/migrate/$type", $namespaces, $module_handler, NULL, $annotation);
$this->alterInfo('migrate_' . $type . '_info');
$this->setCacheBackend($cache_backend, 'migrate_plugins_' . $type);
}
/**
* {@inheritdoc}
*/
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;
}
}

View file

@ -0,0 +1,29 @@
<?php
namespace Drupal\migrate\Plugin;
use Drupal\Component\Plugin\PluginManagerInterface;
interface MigratePluginManagerInterface extends PluginManagerInterface {
/**
* Creates a pre-configured instance of a migration plugin.
*
* A specific createInstance method is necessary to pass the migration on.
*
* @param string $plugin_id
* The ID of the plugin being instantiated.
* @param array $configuration
* An array of configuration relevant to the plugin instance.
* @param \Drupal\migrate\Plugin\MigrationInterface $migration
* The migration context in which the plugin will run.
*
* @return object
* A fully configured plugin instance.
*
* @throws \Drupal\Component\Plugin\Exception\PluginException
* If the instance cannot be created, such as if the ID is invalid.
*/
public function createInstance($plugin_id, array $configuration = [], MigrationInterface $migration = NULL);
}

View file

@ -0,0 +1,57 @@
<?php
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,103 @@
<?php
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();
/**
* Adds 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);
/**
* Allows class to decide how it will react when it is treated like a string.
*/
public function __toString();
/**
* Defines the source fields uniquely identifying a source row.
*
* None of these fields should contain a NULL value. If necessary, use
* prepareRow() or hook_migrate_prepare_row() to rewrite NULL values to
* appropriate empty values (such as '' or 0).
*
* @return array[]
* An associative array of field definitions keyed by field ID. Values are
* associative arrays with a structure that contains the field type ('type'
* key). The other keys are the field storage settings as they are returned
* by FieldStorageDefinitionInterface::getSettings(). As an example, for a
* composite source primary key that is defined by an integer and a
* string, the returned value might look like:
* @code
* return [
* 'id' => [
* 'type' => 'integer',
* 'unsigned' => FALSE,
* 'size' => 'big',
* ],
* 'version' => [
* 'type' => 'string',
* 'max_length' => 64,
* 'is_ascii' => TRUE,
* ],
* ];
* @endcode
* If 'type' points to a field plugin with multiple columns and needs to
* refer to a column different than 'value', the key of that column will be
* appended as a suffix to the plugin name, separated by dot ('.'). Example:
* @code
* return [
* 'format' => [
* 'type' => 'text.format',
* ],
* ];
* @endcode
* Additional custom keys/values, that are not part of field storage
* definition, can be passed in definitions. The most common setting, passed
* along the ID definition, is 'alias' used by SqlBase source plugin:
* @code
* return [
* 'nid' => [
* 'type' => 'integer',
* 'alias' => 'n',
* ],
* ];
* @endcode
*
* @see \Drupal\Core\Field\FieldStorageDefinitionInterface::getSettings()
* @see \Drupal\Core\Field\Plugin\Field\FieldType\IntegerItem
* @see \Drupal\Core\Field\Plugin\Field\FieldType\StringItem
* @see \Drupal\text\Plugin\Field\FieldType\TextItem
* @see \Drupal\migrate\Plugin\migrate\source\SqlBase
*/
public function getIds();
}

View file

@ -0,0 +1,73 @@
<?php
namespace Drupal\migrate\Plugin;
use Drupal\Core\Cache\CacheBackendInterface;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\migrate\Plugin\Discovery\AnnotatedClassDiscoveryAutomatedProviders;
use Drupal\Core\Plugin\Discovery\ContainerDerivativeDiscoveryDecorator;
use Drupal\migrate\Plugin\Discovery\ProviderFilterDecorator;
/**
* Plugin manager for migrate source plugins.
*
* @see \Drupal\migrate\Plugin\MigrateSourceInterface
* @see \Drupal\migrate\Plugin\source\SourcePluginBase
* @see \Drupal\migrate\Annotation\MigrateSource
* @see plugin_api
*
* @ingroup migration
*/
class MigrateSourcePluginManager extends MigratePluginManager {
/**
* MigrateSourcePluginManager constructor.
*
* @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.
*/
public function __construct($type, \Traversable $namespaces, CacheBackendInterface $cache_backend, ModuleHandlerInterface $module_handler) {
parent::__construct($type, $namespaces, $cache_backend, $module_handler, 'Drupal\migrate\Annotation\MigrateSource');
}
/**
* {@inheritdoc}
*/
protected function getDiscovery() {
if (!$this->discovery) {
$discovery = new AnnotatedClassDiscoveryAutomatedProviders($this->subdir, $this->namespaces, $this->pluginDefinitionAnnotationName, $this->additionalAnnotationNamespaces);
$this->discovery = new ContainerDerivativeDiscoveryDecorator($discovery);
}
return $this->discovery;
}
/**
* Finds plugin definitions.
*
* @return array
* List of definitions to store in cache.
*
* @todo This is a temporary solution to the fact that migration source
* plugins have more than one provider. This functionality will be moved to
* core in https://www.drupal.org/node/2786355.
*/
protected function findDefinitions() {
$definitions = $this->getDiscovery()->getDefinitions();
foreach ($definitions as $plugin_id => &$definition) {
$this->processDefinition($definition, $plugin_id);
}
$this->alterDefinitions($definitions);
return ProviderFilterDecorator::filterDefinitions($definitions, function ($provider) {
return $this->providerExists($provider);
});
}
}

View file

@ -0,0 +1,678 @@
<?php
namespace Drupal\migrate\Plugin;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Drupal\Core\Plugin\PluginBase;
use Drupal\migrate\Exception\RequirementsException;
use Drupal\migrate\MigrateException;
use Drupal\migrate\MigrateSkipRowException;
use Drupal\Component\Utility\NestedArray;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Defines the Migration plugin.
*
* The migration process plugin represents one single migration and acts like a
* container for the information about a single migration such as the source,
* process and destination plugins.
*/
class Migration extends PluginBase implements MigrationInterface, RequirementsInterface, ContainerFactoryPluginInterface {
/**
* 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 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 = [];
/**
* Specify value of source_row_status for current map row. Usually set by
* MigrateFieldHandler implementations.
*
* @var int
*/
protected $sourceRowStatus = MigrateIdMapInterface::STATUS_IMPORTED;
/**
* 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 = [];
/**
* An optional list of tags, used by the plugin manager for filtering.
*
* @var array
*/
protected $migration_tags = [];
/**
* 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 migration plugin manager for loading other migration plugins.
*
* @var \Drupal\migrate\Plugin\MigrationPluginManagerInterface
*/
protected $migrationPluginManager;
/**
* The source plugin manager.
*
* @var \Drupal\migrate\Plugin\MigratePluginManager
*/
protected $sourcePluginManager;
/**
* Thep process plugin manager.
*
* @var \Drupal\migrate\Plugin\MigratePluginManager
*/
protected $processPluginManager;
/**
* The destination plugin manager.
*
* @var \Drupal\migrate\Plugin\MigrateDestinationPluginManager
*/
protected $destinationPluginManager;
/**
* The ID map plugin manager.
*
* @var \Drupal\migrate\Plugin\MigratePluginManager
*/
protected $idMapPluginManager;
/**
* Labels corresponding to each defined status.
*
* @var array
*/
protected $statusLabels = [
self::STATUS_IDLE => 'Idle',
self::STATUS_IMPORTING => 'Importing',
self::STATUS_ROLLING_BACK => 'Rolling back',
self::STATUS_STOPPING => 'Stopping',
self::STATUS_DISABLED => 'Disabled',
];
/**
* Constructs a Migration.
*
* @param array $configuration
* Plugin configuration.
* @param string $plugin_id
* The plugin ID.
* @param mixed $plugin_definition
* The plugin definition.
* @param \Drupal\migrate\Plugin\MigrationPluginManagerInterface $migration_plugin_manager
* The migration plugin manager.
* @param \Drupal\migrate\Plugin\MigratePluginManagerInterface $source_plugin_manager
* The source migration plugin manager.
* @param \Drupal\migrate\Plugin\MigratePluginManagerInterface $process_plugin_manager
* The process migration plugin manager.
* @param \Drupal\migrate\Plugin\MigrateDestinationPluginManager $destination_plugin_manager
* The destination migration plugin manager.
* @param \Drupal\migrate\Plugin\MigratePluginManagerInterface $idmap_plugin_manager
* The ID map migration plugin manager.
*/
public function __construct(array $configuration, $plugin_id, $plugin_definition, MigrationPluginManagerInterface $migration_plugin_manager, MigratePluginManagerInterface $source_plugin_manager, MigratePluginManagerInterface $process_plugin_manager, MigrateDestinationPluginManager $destination_plugin_manager, MigratePluginManagerInterface $idmap_plugin_manager) {
parent::__construct($configuration, $plugin_id, $plugin_definition);
$this->migrationPluginManager = $migration_plugin_manager;
$this->sourcePluginManager = $source_plugin_manager;
$this->processPluginManager = $process_plugin_manager;
$this->destinationPluginManager = $destination_plugin_manager;
$this->idMapPluginManager = $idmap_plugin_manager;
foreach (NestedArray::mergeDeep($plugin_definition, $configuration) as $key => $value) {
$this->$key = $value;
}
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
return new static(
$configuration,
$plugin_id,
$plugin_definition,
$container->get('plugin.manager.migration'),
$container->get('plugin.manager.migrate.source'),
$container->get('plugin.manager.migrate.process'),
$container->get('plugin.manager.migrate.destination'),
$container->get('plugin.manager.migrate.id_map')
);
}
/**
* {@inheritdoc}
*/
public function id() {
return $this->pluginId;
}
/**
* {@inheritdoc}
*/
public function label() {
return $this->label;
}
/**
* Gets any arbitrary property's value.
*
* @param string $property
* The property to retrieve.
*
* @return mixed
* The value for that property, or NULL if the property does not exist.
*
* @deprecated in Drupal 8.1.x, will be removed before Drupal 9.0.x. Use
* more specific getters instead.
*/
public function get($property) {
return isset($this->$property) ? $this->$property : NULL;
}
/**
* Retrieves the ID map plugin.
*
* @return \Drupal\migrate\Plugin\MigrateIdMapInterface
* The ID map plugin.
*/
public function getIdMapPlugin() {
return $this->idMapPlugin;
}
/**
* {@inheritdoc}
*/
public function getSourcePlugin() {
if (!isset($this->sourcePlugin)) {
$this->sourcePlugin = $this->sourcePluginManager->createInstance($this->source['plugin'], $this->source, $this);
}
return $this->sourcePlugin;
}
/**
* {@inheritdoc}
*/
public function getProcessPlugins(array $process = NULL) {
if (!isset($process)) {
$process = $this->getProcess();
}
$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][] = $this->processPluginManager->createInstance('get', $configuration, $this);
}
// Get is already handled.
if ($configuration['plugin'] != 'get') {
$this->processPlugins[$index][$property][] = $this->processPluginManager->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 ($stub_being_requested && !empty($this->destination['no_stub'])) {
throw new MigrateSkipRowException();
}
if (!isset($this->destinationPlugin)) {
$this->destinationPlugin = $this->destinationPluginManager->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 = $this->idMapPluginManager->createInstance($plugin, $configuration, $this);
}
return $this->idMapPlugin;
}
/**
* {@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();
}
if (empty($this->requirements)) {
// There are no requirements to check.
return;
}
/** @var \Drupal\migrate\Plugin\MigrationInterface[] $required_migrations */
$required_migrations = $this->getMigrationPluginManager()->createInstances($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->allRowsProcessed()) {
$missing_migrations[] = $migration_id;
}
}
if ($missing_migrations) {
throw new RequirementsException('Missing migrations ' . implode(', ', $missing_migrations) . '.', ['requirements' => $missing_migrations]);
}
}
/**
* Gets the migration plugin manager.
*
* @return \Drupal\migrate\Plugin\MigratePluginManager
* The plugin manager.
*/
protected function getMigrationPluginManager() {
return $this->migrationPluginManager;
}
/**
* {@inheritdoc}
*/
public function setStatus($status) {
\Drupal::keyValue('migrate_status')->set($this->id(), $status);
}
/**
* {@inheritdoc}
*/
public function getStatus() {
return \Drupal::keyValue('migrate_status')->get($this->id(), static::STATUS_IDLE);
}
/**
* {@inheritdoc}
*/
public function getStatusLabel() {
$status = $this->getStatus();
if (isset($this->statusLabels[$status])) {
return $this->statusLabels[$status];
}
else {
return '';
}
}
/**
* {@inheritdoc}
*/
public function getInterruptionResult() {
return \Drupal::keyValue('migrate_interruption_result')->get($this->id(), static::RESULT_INCOMPLETE);
}
/**
* {@inheritdoc}
*/
public function clearInterruptionResult() {
\Drupal::keyValue('migrate_interruption_result')->delete($this->id());
}
/**
* {@inheritdoc}
*/
public function interruptMigration($result) {
$this->setStatus(MigrationInterface::STATUS_STOPPING);
\Drupal::keyValue('migrate_interruption_result')->set($this->id(), $result);
}
/**
* {@inheritdoc}
*/
public function allRowsProcessed() {
$source_count = $this->getSourcePlugin()->count();
// If the source is uncountable, we have no way of knowing if it's
// complete, so stipulate that it is.
if ($source_count < 0) {
return TRUE;
}
$processed_count = $this->getIdMap()->processedCount();
// We don't use == because in some circumstances (like unresolved stubs
// being created), the processed count may be higher than the available
// source rows.
return $source_count <= $processed_count;
}
/**
* {@inheritdoc}
*/
public function set($property_name, $value) {
if ($property_name == 'source') {
// Invalidate the source plugin.
unset($this->sourcePlugin);
}
elseif ($property_name === 'destination') {
// Invalidate the destination plugin.
unset($this->destinationPlugin);
}
$this->{$property_name} = $value;
return $this;
}
/**
* {@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 isTrackLastImported() {
return $this->trackLastImported;
}
/**
* {@inheritdoc}
*/
public function setTrackLastImported($track_last_imported) {
$this->trackLastImported = (bool) $track_last_imported;
return $this;
}
/**
* {@inheritdoc}
*/
public function getMigrationDependencies() {
$this->migration_dependencies = ($this->migration_dependencies ?: []) + ['required' => [], 'optional' => []];
$this->migration_dependencies['optional'] = array_unique(array_merge($this->migration_dependencies['optional'], $this->findMigrationDependencies($this->process)));
return $this->migration_dependencies;
}
/**
* Find migration dependencies from the migration and the iterator plugins.
*
* @param $process
* @return array
*/
protected function findMigrationDependencies($process) {
$return = [];
foreach ($this->getProcessNormalized($process) as $process_pipeline) {
foreach ($process_pipeline as $plugin_configuration) {
if ($plugin_configuration['plugin'] == 'migration') {
$return = array_merge($return, (array) $plugin_configuration['migration']);
}
if ($plugin_configuration['plugin'] == 'iterator') {
$return = array_merge($return, $this->findMigrationDependencies($plugin_configuration['process']));
}
}
}
return $return;
}
/**
* {@inheritdoc}
*/
public function getPluginDefinition() {
$definition = [];
// While normal plugins do not change their definitions on the fly, this
// one does so accommodate for that.
foreach (parent::getPluginDefinition() as $key => $value) {
$definition[$key] = isset($this->$key) ? $this->$key : $value;
}
return $definition;
}
/**
* {@inheritdoc}
*/
public function getDestinationConfiguration() {
return $this->destination;
}
/**
* {@inheritdoc}
*/
public function getSourceConfiguration() {
return $this->source;
}
/**
* {@inheritdoc}
*/
public function getTrackLastImported() {
return $this->trackLastImported;
}
/**
* {@inheritdoc}
*/
public function getDestinationIds() {
return $this->destinationIds;
}
/**
* {@inheritdoc}
*/
public function getMigrationTags() {
return $this->migration_tags;
}
}

View file

@ -0,0 +1,32 @@
<?php
namespace Drupal\migrate\Plugin;
/**
* Provides functionality for migration derivers.
*/
trait MigrationDeriverTrait {
/**
* Returns a fully initialized instance of a source plugin.
*
* @param string $source_plugin_id
* The source plugin ID.
*
* @return \Drupal\migrate\Plugin\MigrateSourceInterface|\Drupal\migrate\Plugin\RequirementsInterface
* The fully initialized source plugin.
*/
public static function getSourcePlugin($source_plugin_id) {
$definition = [
'source' => [
'ignore_map' => TRUE,
'plugin' => $source_plugin_id,
],
'destination' => [
'plugin' => 'null',
],
];
return \Drupal::service('plugin.manager.migration')->createStubMigration($definition)->getSourcePlugin();
}
}

View file

@ -0,0 +1,325 @@
<?php
namespace Drupal\migrate\Plugin;
use Drupal\Component\Plugin\DerivativeInspectionInterface;
use Drupal\Component\Plugin\PluginInspectionInterface;
/**
* Interface for migrations.
*/
interface MigrationInterface extends PluginInspectionInterface, DerivativeInspectionInterface {
/**
* 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;
/**
* An alias for getPluginId() for backwards compatibility reasons.
*
* @return string
* The plugin_id of the plugin instance.
*
* @see \Drupal\migrate\Plugin\MigrationInterface::getPluginId()
*/
public function id();
/**
* Get the plugin label.
*
* @return string
* The label for this migration.
*/
public function label();
/**
* 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();
/**
* Check if all source rows from this migration have been processed.
*
* @return bool
* TRUE if this migration is complete otherwise FALSE.
*/
public function allRowsProcessed();
/**
* Set the current migration status.
*
* @param int $result
* One of the STATUS_* constants.
*/
public function setStatus($status);
/**
* Get the current migration status.
*
* @return int
* The current migration status. Defaults to STATUS_IDLE.
*/
public function getStatus();
/**
* Retrieve a label for the current status.
*
* @return string
* User-friendly string corresponding to a STATUS_ constant.
*/
public function getStatusLabel();
/**
* Get the result to return upon interruption.
*
* @return int
* The current interruption result. Defaults to RESULT_INCOMPLETE.
*/
public function getInterruptionResult();
/**
* Clears the result to return upon interruption.
*/
public function clearInterruptionResult();
/**
* Signal that the migration should be interrupted with the specified result
* code.
*
* @param int $result
* One of the MigrationInterface::RESULT_* constants.
*/
public function interruptMigration($result);
/**
* 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);
/**
* 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();
/**
* Get the destination configuration, with at least a 'plugin' key.
*
* @return array
* The destination configuration.
*/
public function getDestinationConfiguration();
/**
* Get the source configuration, with at least a 'plugin' key.
*
* @return array
* The source configuration.
*/
public function getSourceConfiguration();
/**
* If true, track time of last import.
*
* @return bool
* Flag to determine desire of tracking time of last import.
*/
public function getTrackLastImported();
/**
* 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.
*
* @return array
* Destination identifiers.
*/
public function getDestinationIds();
/**
* The migration tags.
*
* @return array
* Migration tags.
*/
public function getMigrationTags();
}

View file

@ -0,0 +1,261 @@
<?php
namespace Drupal\migrate\Plugin;
use Drupal\Component\Graph\Graph;
use Drupal\Component\Plugin\PluginBase;
use Drupal\Core\Cache\CacheBackendInterface;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\Language\LanguageManagerInterface;
use Drupal\Core\Plugin\DefaultPluginManager;
use Drupal\Core\Plugin\Discovery\ContainerDerivativeDiscoveryDecorator;
use Drupal\migrate\Plugin\Discovery\ProviderFilterDecorator;
use Drupal\Core\Plugin\Discovery\YamlDirectoryDiscovery;
use Drupal\Core\Plugin\Factory\ContainerFactory;
use Drupal\migrate\MigrateBuildDependencyInterface;
/**
* Plugin manager for migration plugins.
*/
class MigrationPluginManager extends DefaultPluginManager implements MigrationPluginManagerInterface, MigrateBuildDependencyInterface {
/**
* Provides default values for migrations.
*
* @var array
*/
protected $defaults = array(
'class' => '\Drupal\migrate\Plugin\Migration',
);
/**
* The interface the plugins should implement.
*
* @var string
*/
protected $pluginInterface = 'Drupal\migrate\Plugin\MigrationInterface';
/**
* The module handler.
*
* @var \Drupal\Core\Extension\ModuleHandlerInterface
*/
protected $moduleHandler;
/**
* Construct a migration plugin manager.
*
* @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
* The module handler.
* @param \Drupal\Core\Cache\CacheBackendInterface $cache_backend
* The cache backend for the definitions.
* @param \Drupal\Core\Language\LanguageManagerInterface $language_manager
* The language manager.
*/
public function __construct(ModuleHandlerInterface $module_handler, CacheBackendInterface $cache_backend, LanguageManagerInterface $language_manager) {
$this->factory = new ContainerFactory($this, $this->pluginInterface);
$this->alterInfo('migration_plugins');
$this->setCacheBackend($cache_backend, 'migration_plugins', array('migration_plugins'));
$this->moduleHandler = $module_handler;
}
/**
* {@inheritdoc}
*/
protected function getDiscovery() {
if (!isset($this->discovery)) {
$directories = array_map(function($directory) {
return [$directory . '/migration_templates', $directory . '/migrations'];
}, $this->moduleHandler->getModuleDirectories());
$yaml_discovery = new YamlDirectoryDiscovery($directories, 'migrate');
// This gets rid of migrations which try to use a non-existent source
// plugin. The common case for this is if the source plugin has, or
// specifies, a non-existent provider.
$only_with_source_discovery = new NoSourcePluginDecorator($yaml_discovery);
// This gets rid of migrations with explicit providers set if one of the
// providers do not exist before we try to use a potentially non-existing
// deriver. This is a rare case.
$filtered_discovery = new ProviderFilterDecorator($only_with_source_discovery, [$this->moduleHandler, 'moduleExists']);
$this->discovery = new ContainerDerivativeDiscoveryDecorator($filtered_discovery);
}
return $this->discovery;
}
/**
* {@inheritdoc}
*/
public function createInstance($plugin_id, array $configuration = array()) {
$instances = $this->createInstances([$plugin_id], [$plugin_id => $configuration]);
return reset($instances);
}
/**
* {@inheritdoc}
*/
public function createInstances($migration_id, array $configuration = array()) {
if (empty($migration_id)) {
$migration_id = array_keys($this->getDefinitions());
}
$factory = $this->getFactory();
$migration_ids = (array) $migration_id;
$plugin_ids = $this->expandPluginIds($migration_ids);
$instances = [];
foreach ($plugin_ids as $plugin_id) {
$instances[$plugin_id] = $factory->createInstance($plugin_id, isset($configuration[$plugin_id]) ? $configuration[$plugin_id] : []);
}
foreach ($instances as $migration) {
$migration->set('migration_dependencies', array_map([$this, 'expandPluginIds'], $migration->getMigrationDependencies()));
}
// Sort the migrations based on their dependencies.
return $this->buildDependencyMigration($instances, []);
}
/**
* Create migrations given a tag.
*
* @param string $tag
* A migration tag we want to filter by.
*
* @return array|\Drupal\migrate\Plugin\MigrationInterface[]
* An array of migration objects with the given tag.
*/
public function createInstancesByTag($tag) {
$migrations = array_filter($this->getDefinitions(), function($migration) use ($tag) {
return !empty($migration['migration_tags']) && in_array($tag, $migration['migration_tags']);
});
return $this->createInstances(array_keys($migrations));
}
/**
* Expand derivative migration dependencies.
*
* We need to expand any derivative migrations. Derivative migrations are
* calculated by migration derivers such as D6NodeDeriver. This allows
* migrations to depend on the base id and then have a dependency on all
* derivative migrations. For example, d6_comment depends on d6_node but after
* we've expanded the dependencies it will depend on d6_node:page,
* d6_node:story and so on, for other derivative migrations.
*
* @return array
* An array of expanded plugin ids.
*/
protected function expandPluginIds(array $migration_ids) {
$plugin_ids = [];
foreach ($migration_ids as $id) {
$plugin_ids += preg_grep('/^' . preg_quote($id, '/') . PluginBase::DERIVATIVE_SEPARATOR . '/', array_keys($this->getDefinitions()));
if ($this->hasDefinition($id)) {
$plugin_ids[] = $id;
}
}
return $plugin_ids;
}
/**
* {@inheritdoc}
*/
public function buildDependencyMigration(array $migrations, array $dynamic_ids) {
// Migration dependencies 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 = [];
$required_dependency_graph = [];
$have_optional = FALSE;
foreach ($migrations as $migration) {
/** @var \Drupal\migrate\Plugin\MigrationInterface $migration */
$id = $migration->id();
$requirements[$id] = [];
$dependency_graph[$id]['edges'] = [];
$migration_dependencies = $migration->getMigrationDependencies();
if (isset($migration_dependencies['required'])) {
foreach ($migration_dependencies['required'] as $dependency) {
if (!isset($dynamic_ids[$dependency])) {
$this->addDependency($required_dependency_graph, $id, $dependency, $dynamic_ids);
}
$this->addDependency($dependency_graph, $id, $dependency, $dynamic_ids);
}
}
if (!empty($migration_dependencies['optional'])) {
foreach ($migration_dependencies['optional'] as $dependency) {
$this->addDependency($dependency_graph, $id, $dependency, $dynamic_ids);
}
$have_optional = TRUE;
}
}
$dependency_graph = (new Graph($dependency_graph))->searchAndSort();
if ($have_optional) {
$required_dependency_graph = (new Graph($required_dependency_graph))->searchAndSort();
}
else {
$required_dependency_graph = $dependency_graph;
}
$weights = [];
foreach ($migrations as $migration_id => $migration) {
// Populate a weights array to use with array_multisort() later.
$weights[] = $dependency_graph[$migration_id]['weight'];
if (!empty($required_dependency_graph[$migration_id]['paths'])) {
$migration->set('requirements', $required_dependency_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, passed by reference.
* @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);
}
/**
* {@inheritdoc}
*/
public function createStubMigration(array $definition) {
$id = isset($definition['id']) ? $definition['id'] : uniqid();
return Migration::create(\Drupal::getContainer(), [], $id, $definition);
}
/**
* Finds plugin definitions.
*
* @return array
* List of definitions to store in cache.
*
* @todo This is a temporary solution to the fact that migration source
* plugins have more than one provider. This functionality will be moved to
* core in https://www.drupal.org/node/2786355.
*/
protected function findDefinitions() {
$definitions = $this->getDiscovery()->getDefinitions();
foreach ($definitions as $plugin_id => &$definition) {
$this->processDefinition($definition, $plugin_id);
}
$this->alterDefinitions($definitions);
return ProviderFilterDecorator::filterDefinitions($definitions, function ($provider) {
return $this->providerExists($provider);
});
}
}

View file

@ -0,0 +1,43 @@
<?php
namespace Drupal\migrate\Plugin;
use Drupal\Component\Plugin\PluginManagerInterface;
/**
* Migration plugin manager interface.
*/
interface MigrationPluginManagerInterface extends PluginManagerInterface {
/**
* Create pre-configured instance of plugin derivatives.
*
* @param array $id
* Either the plugin ID or the base plugin ID of the plugins being
* instantiated. Also accepts an array of plugin IDs and an empty array to
* load all plugins.
* @param array $configuration
* An array of configuration relevant to the plugin instances. Keyed by the
* plugin ID.
*
* @return \Drupal\migrate\Plugin\MigrationInterface[]
* Fully configured plugin instances.
*
* @throws \Drupal\Component\Plugin\Exception\PluginException
* If an instance cannot be created, such as if the ID is invalid.
*/
public function createInstances($id, array $configuration = array());
/**
* Creates a stub migration plugin from a definition array.
*
* @param array $definition
* The migration definition. If an 'id' key is set then this will be used as
* the migration ID, if not a random ID will be assigned.
*
* @return \Drupal\migrate\Plugin\Migration
* The stub migration.
*/
public function createStubMigration(array $definition);
}

View file

@ -0,0 +1,58 @@
<?php
namespace Drupal\migrate\Plugin;
use Drupal\Component\Plugin\Discovery\DiscoveryInterface;
use Drupal\Component\Plugin\Discovery\DiscoveryTrait;
/**
* Remove definitions which refer to a non-existing source plugin.
*/
class NoSourcePluginDecorator implements DiscoveryInterface {
use DiscoveryTrait;
/**
* The Discovery object being decorated.
*
* @var \Drupal\Component\Plugin\Discovery\DiscoveryInterface
*/
protected $decorated;
/**
* Constructs a NoSourcePluginDecorator object.
*
* @param \Drupal\Component\Plugin\Discovery\DiscoveryInterface $decorated
* The object implementing DiscoveryInterface that is being decorated.
*/
public function __construct(DiscoveryInterface $decorated) {
$this->decorated = $decorated;
}
/**
* {@inheritdoc}
*/
public function getDefinitions() {
/** @var \Drupal\Component\Plugin\PluginManagerInterface $source_plugin_manager */
$source_plugin_manager = \Drupal::service('plugin.manager.migrate.source');
return array_filter($this->decorated->getDefinitions(), function (array $definition) use ($source_plugin_manager) {
return $source_plugin_manager->hasDefinition($definition['source']['plugin']);
});
}
/**
* Passes through all unknown calls onto the decorated object.
*
* @param string $method
* The method to call on the decorated object.
* @param array $args
* Call arguments.
*
* @return mixed
* The return value from the method on the decorated object.
*/
public function __call($method, array $args) {
return call_user_func_array([$this->decorated, $method], $args);
}
}

View file

@ -0,0 +1,94 @@
<?php
namespace Drupal\migrate\Plugin;
use Drupal\migrate\Event\ImportAwareInterface;
use Drupal\migrate\Event\MigrateEvents;
use Drupal\migrate\Event\MigrateImportEvent;
use Drupal\migrate\Event\MigrateRollbackEvent;
use Drupal\migrate\Event\RollbackAwareInterface;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
/**
* Event subscriber to forward Migrate events to source and destination plugins.
*/
class PluginEventSubscriber implements EventSubscriberInterface {
/**
* Tries to invoke event handling methods on source and destination plugins.
*
* @param string $method
* The method to invoke.
* @param \Drupal\migrate\Event\MigrateImportEvent|\Drupal\migrate\Event\MigrateRollbackEvent $event
* The event that has triggered the invocation.
* @param string $plugin_interface
* The interface which plugins must implement in order to be invoked.
*/
protected function invoke($method, $event, $plugin_interface) {
$migration = $event->getMigration();
$source = $migration->getSourcePlugin();
if ($source instanceof $plugin_interface) {
call_user_func([$source, $method], $event);
}
$destination = $migration->getDestinationPlugin();
if ($destination instanceof $plugin_interface) {
call_user_func([$destination, $method], $event);
}
}
/**
* Forwards pre-import events to the source and destination plugins.
*
* @param \Drupal\migrate\Event\MigrateImportEvent $event
* The import event.
*/
public function preImport(MigrateImportEvent $event) {
$this->invoke('preImport', $event, ImportAwareInterface::class);
}
/**
* Forwards post-import events to the source and destination plugins.
*
* @param \Drupal\migrate\Event\MigrateImportEvent $event
* The import event.
*/
public function postImport(MigrateImportEvent $event) {
$this->invoke('postImport', $event, ImportAwareInterface::class);
}
/**
* Forwards pre-rollback events to the source and destination plugins.
*
* @param \Drupal\migrate\Event\MigrateRollbackEvent $event
* The rollback event.
*/
public function preRollback(MigrateRollbackEvent $event) {
$this->invoke('preRollback', $event, RollbackAwareInterface::class);
}
/**
* Forwards post-rollback events to the source and destination plugins.
*
* @param \Drupal\migrate\Event\MigrateRollbackEvent $event
* The rollback event.
*/
public function postRollback(MigrateRollbackEvent $event) {
$this->invoke('postRollback', $event, RollbackAwareInterface::class);
}
/**
* {@inheritdoc}
*/
public static function getSubscribedEvents() {
$events = [];
$events[MigrateEvents::PRE_IMPORT][] = ['preImport'];
$events[MigrateEvents::POST_IMPORT][] = ['postImport'];
$events[MigrateEvents::PRE_ROLLBACK][] = ['preRollback'];
$events[MigrateEvents::POST_ROLLBACK][] = ['postRollback'];
return $events;
}
}

View file

@ -0,0 +1,18 @@
<?php
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,69 @@
<?php
namespace Drupal\migrate\Plugin\migrate\destination;
use Drupal\migrate\Plugin\MigrationInterface;
use Drupal\migrate\Row;
/**
* Defines the base abstract class for component entity display.
*/
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.
}
/**
* Gets 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,152 @@
<?php
namespace Drupal\migrate\Plugin\migrate\destination;
use Drupal\Component\Plugin\DependentPluginInterface;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\Entity\DependencyTrait;
use Drupal\Core\Language\LanguageManagerInterface;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Drupal\migrate\Plugin\MigrationInterface;
use Drupal\migrate\Row;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Provides Configuration Management destination plugin.
*
* 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;
/**
* The language manager.
*
* @var \Drupal\Core\Language\LanguageManagerInterface
*/
protected $language_manager;
/**
* 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\Plugin\MigrationInterface $migration
* The migration entity.
* @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
* The configuration factory.
* @param \Drupal\Core\Language\LanguageManagerInterface $language_manager
* The language manager.
*/
public function __construct(array $configuration, $plugin_id, $plugin_definition, MigrationInterface $migration, ConfigFactoryInterface $config_factory, LanguageManagerInterface $language_manager) {
parent::__construct($configuration, $plugin_id, $plugin_definition, $migration);
$this->config = $config_factory->getEditable($configuration['config_name']);
$this->language_manager = $language_manager;
if ($this->isTranslationDestination()) {
$this->supportsRollback = TRUE;
}
}
/**
* {@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'),
$container->get('language_manager')
);
}
/**
* {@inheritdoc}
*/
public function import(Row $row, array $old_destination_id_values = array()) {
if ($this->isTranslationDestination()) {
$this->config = $this->language_manager->getLanguageConfigOverride($row->getDestinationProperty('langcode'), $this->config->getName());
}
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();
$ids[] = $this->config->getName();
if ($this->isTranslationDestination()) {
$ids[] = $row->getDestinationProperty('langcode');
}
return $ids;
}
/**
* {@inheritdoc}
*/
public function fields(MigrationInterface $migration = NULL) {
// @todo Dynamically fetch fields using Config Schema API.
}
/**
* {@inheritdoc}
*/
public function getIds() {
$ids['config_name']['type'] = 'string';
if ($this->isTranslationDestination()) {
$ids['langcode']['type'] = 'string';
}
return $ids;
}
/**
* {@inheritdoc}
*/
public function calculateDependencies() {
$provider = explode('.', $this->config->getName(), 2)[0];
$this->addDependency('module', $provider);
return $this->dependencies;
}
/**
* Get whether this destination is for translations.
*
* @return bool
* Whether this destination is for translations.
*/
protected function isTranslationDestination() {
return !empty($this->configuration['translations']);
}
/**
* {@inheritdoc}
*/
public function rollback(array $destination_identifier) {
if ($this->isTranslationDestination()) {
$language = $destination_identifier['langcode'];
$config = $this->language_manager->getLanguageConfigOverride($language, $this->config->getName());
$config->delete();
}
}
}

View file

@ -0,0 +1,113 @@
<?php
namespace Drupal\migrate\Plugin\migrate\destination;
use Drupal\Core\Plugin\PluginBase;
use Drupal\migrate\Plugin\MigrationInterface;
use Drupal\migrate\Exception\RequirementsException;
use Drupal\migrate\Plugin\MigrateDestinationInterface;
use Drupal\migrate\Plugin\MigrateIdMapInterface;
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 {
/**
* Indicates whether the destination can be rolled back.
*
* @var bool
*/
protected $supportsRollback = FALSE;
/**
* The rollback action to be saved for the last imported item.
*
* @var int
*/
protected $rollbackAction = MigrateIdMapInterface::ROLLBACK_DELETE;
/**
* The migration.
*
* @var \Drupal\migrate\Plugin\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 \Drupal\migrate\Plugin\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 rollbackAction() {
return $this->rollbackAction;
}
/**
* {@inheritdoc}
*/
public function checkRequirements() {
if (empty($this->pluginDefinition['requirements_met'])) {
throw new RequirementsException();
}
}
/**
* {@inheritdoc}
*/
public function rollback(array $destination_identifier) {
// By default we do nothing.
}
/**
* {@inheritdoc}
*/
public function supportsRollback() {
return $this->supportsRollback;
}
/**
* For a destination item being updated, set the appropriate rollback action.
*
* @param array $id_map
* The map row data for the item.
* @param int $update_action
* The rollback action to take if we are updating an existing item.
*/
protected function setRollbackAction(array $id_map, $update_action = MigrateIdMapInterface::ROLLBACK_PRESERVE) {
// If the entity we're updating was previously migrated by us, preserve the
// existing rollback action.
if (isset($id_map['sourceid1'])) {
$this->rollbackAction = $id_map['rollback_action'];
}
// Otherwise, we're updating an entity which already existed on the
// destination and want to make sure we do not delete it on rollback.
else {
$this->rollbackAction = $update_action;
}
}
}

View file

@ -0,0 +1,193 @@
<?php
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\Plugin\MigrationInterface;
use Drupal\migrate\Row;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Provides entity destination plugin.
*
* @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;
$this->supportsRollback = TRUE;
}
/**
* {@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);
}
/**
* Gets the bundle for the row taking into account the default.
*
* @param \Drupal\migrate\Row $row
* The current row we're importing.
*
* @return string
* The bundle for this row.
*/
public function getBundle(Row $row) {
$default_bundle = isset($this->configuration['default_bundle']) ? $this->configuration['default_bundle'] : '';
$bundle_key = $this->getKey('bundle');
return $row->getDestinationProperty($bundle_key) ?: $default_bundle;
}
/**
* {@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 are importing into.
*/
protected function getEntity(Row $row, array $old_destination_id_values) {
$entity_id = reset($old_destination_id_values) ?: $this->getEntityId($row);
if (!empty($entity_id) && ($entity = $this->storage->load($entity_id))) {
// Allow updateEntity() to change the entity.
$entity = $this->updateEntity($entity, $row) ?: $entity;
}
else {
// Attempt to ensure we always have a bundle.
if ($bundle = $this->getBundle($row)) {
$row->setDestinationProperty($this->getKey('bundle'), $bundle);
}
// Stubs might need some required fields filled in.
if ($row->isStub()) {
$this->processStubRow($row);
}
$entity = $this->storage->create($row->getDestination());
$entity->enforceIsNew();
}
return $entity;
}
/**
* Gets the entity ID of the row.
*
* @param \Drupal\migrate\Row $row
* The row of data.
*
* @return string
* The entity ID for the row that we are importing.
*/
protected function getEntityId(Row $row) {
return $row->getDestinationProperty($this->getKey('id'));
}
/**
* 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 rollback(array $destination_identifier) {
// Delete the specified entity from Drupal if it exists.
$entity = $this->storage->load(reset($destination_identifier));
if ($entity) {
$entity->delete();
}
}
/**
* {@inheritdoc}
*/
public function calculateDependencies() {
$this->addDependency('module', $this->storage->getEntityType()->getProvider());
return $this->dependencies;
}
}

View file

@ -0,0 +1,26 @@
<?php
namespace Drupal\migrate\Plugin\migrate\destination;
use Drupal\migrate\Row;
/**
* Provides entity base field override plugin.
*
* @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,121 @@
<?php
namespace Drupal\migrate\Plugin\migrate\destination;
use Drupal\Component\Utility\NestedArray;
use Drupal\Core\Entity\EntityInterface;
use Drupal\migrate\MigrateException;
use Drupal\migrate\Plugin\MigrateIdMapInterface;
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.');
}
$this->rollbackAction = MigrateIdMapInterface::ROLLBACK_DELETE;
$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);
}
$this->setRollbackAction($row->getIdMap());
}
/**
* 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);
}
/**
* Generates 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,292 @@
<?php
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\Field\FieldTypePluginManagerInterface;
use Drupal\Core\TypedData\TranslatableInterface;
use Drupal\Core\TypedData\TypedDataInterface;
use Drupal\migrate\Plugin\MigrationInterface;
use Drupal\migrate\MigrateException;
use Drupal\migrate\Plugin\MigrateIdMapInterface;
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;
/**
* Field type plugin manager.
*
* @var \Drupal\Core\Field\FieldTypePluginManagerInterface
*/
protected $fieldTypeManager;
/**
* 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\Plugin\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\Core\Entity\EntityManagerInterface $entity_manager
* The entity manager service.
* @param \Drupal\Core\Field\FieldTypePluginManagerInterface $field_type_manager
* The field type plugin manager service.
*/
public function __construct(array $configuration, $plugin_id, $plugin_definition, MigrationInterface $migration, EntityStorageInterface $storage, array $bundles, EntityManagerInterface $entity_manager, FieldTypePluginManagerInterface $field_type_manager) {
parent::__construct($configuration, $plugin_id, $plugin_definition, $migration, $storage, $bundles);
$this->entityManager = $entity_manager;
$this->fieldTypeManager = $field_type_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'),
$container->get('plugin.manager.field.field_type')
);
}
/**
* {@inheritdoc}
*/
public function import(Row $row, array $old_destination_id_values = array()) {
$this->rollbackAction = MigrateIdMapInterface::ROLLBACK_DELETE;
$entity = $this->getEntity($row, $old_destination_id_values);
if (!$entity) {
throw new MigrateException('Unable to get entity');
}
$ids = $this->save($entity, $old_destination_id_values);
if (!empty($this->configuration['translations'])) {
$ids[] = $entity->language()->getId();
}
return $ids;
}
/**
* Saves the entity.
*
* @param \Drupal\Core\Entity\ContentEntityInterface $entity
* The content entity.
* @param array $old_destination_id_values
* (optional) An array of destination ID values. Defaults to an empty array.
*
* @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());
}
/**
* Get whether this destination is for translations.
*
* @return bool
* Whether this destination is for translations.
*/
protected function isTranslationDestination() {
return !empty($this->configuration['translations']);
}
/**
* {@inheritdoc}
*/
public function getIds() {
$id_key = $this->getKey('id');
$ids[$id_key] = $this->getDefinitionFromEntity($id_key);
if ($this->isTranslationDestination()) {
if (!$langcode_key = $this->getKey('langcode')) {
throw new MigrateException('This entity type does not support translation.');
}
$ids[$langcode_key] = $this->getDefinitionFromEntity($langcode_key);
}
return $ids;
}
/**
* Updates 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.
*
* @return \Drupal\Core\Entity\EntityInterface|null
* An updated entity, or NULL if it's the same as the one passed in.
*/
protected function updateEntity(EntityInterface $entity, Row $row) {
// By default, an update will be preserved.
$rollback_action = MigrateIdMapInterface::ROLLBACK_PRESERVE;
// Make sure we have the right translation.
if ($this->isTranslationDestination()) {
$property = $this->storage->getEntityType()->getKey('langcode');
if ($row->hasDestinationProperty($property)) {
$language = $row->getDestinationProperty($property);
if (!$entity->hasTranslation($language)) {
$entity->addTranslation($language);
// We're adding a translation, so delete it on rollback.
$rollback_action = MigrateIdMapInterface::ROLLBACK_DELETE;
}
$entity = $entity->getTranslation($language);
}
}
// If the migration has specified a list of properties to be overwritten,
// clone the row with an empty set of destination values, and re-add only
// the specified properties.
if (isset($this->configuration['overwrite_properties'])) {
$clone = $row->cloneWithoutDestination();
foreach ($this->configuration['overwrite_properties'] as $property) {
$clone->setDestinationProperty($property, $row->getDestinationProperty($property));
}
$row = $clone;
}
foreach ($row->getDestination() as $field_name => $values) {
$field = $entity->$field_name;
if ($field instanceof TypedDataInterface) {
$field->setValue($values);
}
}
$this->setRollbackAction($row->getIdMap(), $rollback_action);
// We might have a different (translated) entity, so return it.
return $entity;
}
/**
* Populates as much of the stub row as possible.
*
* @param \Drupal\migrate\Row $row
* The row of data.
*/
protected function processStubRow(Row $row) {
$bundle_key = $this->getKey('bundle');
if ($bundle_key && empty($row->getDestinationProperty($bundle_key))) {
if (empty($this->bundles)) {
throw new MigrateException('Stubbing failed, no bundles available for entity type: ' . $this->storage->getEntityTypeId());
}
$row->setDestinationProperty($bundle_key, reset($this->bundles));
}
// Populate any required fields not already populated.
$fields = $this->entityManager
->getFieldDefinitions($this->storage->getEntityTypeId(), $bundle_key);
foreach ($fields as $field_name => $field_definition) {
if ($field_definition->isRequired() && is_null($row->getDestinationProperty($field_name))) {
// Use the configured default value for this specific field, if any.
if ($default_value = $field_definition->getDefaultValueLiteral()) {
$values[] = $default_value;
}
else {
// Otherwise, ask the field type to generate a sample value.
$field_type = $field_definition->getType();
/** @var \Drupal\Core\Field\FieldItemInterface $field_type_class */
$field_type_class = $this->fieldTypeManager
->getPluginClass($field_definition->getType());
$values = $field_type_class::generateSampleValue($field_definition);
if (is_null($values)) {
// Handle failure to generate a sample value.
throw new MigrateException('Stubbing failed, unable to generate value for field ' . $field_name);
}
}
$row->setDestinationProperty($field_name, $values);
}
}
}
/**
* {@inheritdoc}
*/
public function rollback(array $destination_identifier) {
if ($this->isTranslationDestination()) {
// Attempt to remove the translation.
$entity = $this->storage->load(reset($destination_identifier));
if ($entity && $entity instanceof TranslatableInterface) {
if ($key = $this->getKey('langcode')) {
if (isset($destination_identifier[$key])) {
$langcode = $destination_identifier[$key];
if ($entity->hasTranslation($langcode)) {
// Make sure we don't remove the default translation.
$translation = $entity->getTranslation($langcode);
if (!$translation->isDefaultTranslation()) {
$entity->removeTranslation($langcode);
$entity->save();
}
}
}
}
}
}
else {
parent::rollback($destination_identifier);
}
}
/**
* Gets the field definition from a specific entity base field.
*
* The method takes the field ID as an argument and returns the field storage
* definition to be used in getIds() by querying the destination entity base
* field definition.
*
* @param string $key
* The field ID key.
*
* @return array
* An associative array with a structure that contains the field type, keyed
* as 'type', together with field storage settings as they are returned by
* FieldStorageDefinitionInterface::getSettings().
*
* @see \Drupal\Core\Field\FieldStorageDefinitionInterface::getSettings()
*/
protected function getDefinitionFromEntity($key) {
$entity_type_id = static::getEntityTypeId($this->getPluginId());
/** @var \Drupal\Core\Field\FieldStorageDefinitionInterface[] $definitions */
$definitions = $this->entityManager->getBaseFieldDefinitions($entity_type_id);
$field_definition = $definitions[$key];
return [
'type' => $field_definition->getType(),
] + $field_definition->getSettings();
}
}

View file

@ -0,0 +1,32 @@
<?php
namespace Drupal\migrate\Plugin\migrate\destination;
/**
* Provides entity field instance plugin.
*
* @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;
}
/**
* {@inheritdoc}
*/
public function rollback(array $destination_identifier) {
$destination_identifier = implode('.', $destination_identifier);
parent::rollback(array($destination_identifier));
}
}

View file

@ -0,0 +1,31 @@
<?php
namespace Drupal\migrate\Plugin\migrate\destination;
/**
* Provides entity field storage configuration plugin.
*
* @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;
}
/**
* {@inheritdoc}
*/
public function rollback(array $destination_identifier) {
$destination_identifier = implode('.', $destination_identifier);
parent::rollback(array($destination_identifier));
}
}

View file

@ -0,0 +1,81 @@
<?php
namespace Drupal\migrate\Plugin\migrate\destination;
use Drupal\Core\Entity\ContentEntityInterface;
use Drupal\migrate\MigrateException;
use Drupal\migrate\Row;
/**
* Provides entity revision destination plugin.
*
* @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);
}
/**
* Gets the entity.
*
* @param \Drupal\migrate\Row $row
* The row object.
* @param array $old_destination_id_values
* The old destination IDs.
*
* @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);
// If we fail to load the original entity something is wrong and we need
// to return immediately.
if (!$entity) {
return FALSE;
}
$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')) {
return [$key => $this->getDefinitionFromEntity($key)];
}
throw new MigrateException('This entity type does not support revisions.');
}
}

View file

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

View file

@ -0,0 +1,38 @@
<?php
namespace Drupal\migrate\Plugin\migrate\destination;
use Drupal\migrate\Plugin\MigrationInterface;
use Drupal\migrate\Row;
/**
* Provides null destination plugin.
*
* @MigrateDestination(
* id = "null",
* requirements_met = false
* )
*/
class NullDestination 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,23 @@
<?php
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,23 @@
<?php
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,923 @@
<?php
namespace Drupal\migrate\Plugin\migrate\id_map;
use Drupal\Component\Utility\Unicode;
use Drupal\Core\Field\BaseFieldDefinition;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Drupal\Core\Plugin\PluginBase;
use Drupal\migrate\Plugin\MigrationInterface;
use Drupal\migrate\Event\MigrateIdMapMessageEvent;
use Drupal\migrate\MigrateException;
use Drupal\migrate\MigrateMessageInterface;
use Drupal\migrate\Plugin\MigrateIdMapInterface;
use Drupal\migrate\Row;
use Drupal\migrate\Event\MigrateEvents;
use Drupal\migrate\Event\MigrateMapSaveEvent;
use Drupal\migrate\Event\MigrateMapDeleteEvent;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
/**
* 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, ContainerFactoryPluginInterface {
/**
* Column name of hashed source id values.
*/
const SOURCE_IDS_HASH = 'source_ids_hash';
/**
* An event dispatcher instance to use for map events.
*
* @var \Symfony\Component\EventDispatcher\EventDispatcherInterface
*/
protected $eventDispatcher;
/**
* 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;
/**
* The select query.
*
* @var \Drupal\Core\Database\Query\SelectInterface
*/
protected $query;
/**
* The migration being done.
*
* @var \Drupal\migrate\Plugin\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\Plugin\MigrationInterface $migration
* The migration to do.
*/
public function __construct(array $configuration, $plugin_id, $plugin_definition, MigrationInterface $migration, EventDispatcherInterface $event_dispatcher) {
parent::__construct($configuration, $plugin_id, $plugin_definition);
$this->migration = $migration;
$this->eventDispatcher = $event_dispatcher;
}
/**
* {@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('event_dispatcher')
);
}
/**
* Retrieves the hash of the source identifier values.
*
* It is public only for testing purposes.
*
* @param array $source_id_values
* The source identifiers
*
* @return string
* An hash containing the hashed values of the source identifiers.
*/
public function getSourceIDsHash(array $source_id_values) {
// When looking up the destination ID we require an array with both the
// source key and value, e.g. ['nid' => 41]. In this case, $source_id_values
// need to be ordered the same order as $this->sourceIdFields().
// However, the Migration process plugin doesn't currently have a way to get
// the source key so we presume the values have been passed through in the
// correct order.
if (!isset($source_id_values[0])) {
$source_id_values_keyed = [];
foreach ($this->sourceIdFields() as $field_name => $source_id) {
$source_id_values_keyed[] = $source_id_values[$field_name];
}
$source_id_values = $source_id_values_keyed;
}
return hash('sha256', serialize(array_map('strval', $source_id_values)));
}
/**
* 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();
foreach ($this->migration->getSourcePlugin()->getIds() as $id_definition) {
$mapkey = 'sourceid' . $count++;
$source_id_schema[$mapkey] = $this->getFieldSchema($id_definition);
$source_id_schema[$mapkey]['not null'] = TRUE;
}
$source_ids_hash[static::SOURCE_IDS_HASH] = array(
'type' => 'varchar',
'length' => '64',
'not null' => TRUE,
'description' => 'Hash of source ids. Used as primary key',
);
$fields = $source_ids_hash + $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,
'primary key' => array(static::SOURCE_IDS_HASH),
);
$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_ids_hash;
$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'),
);
$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',
)
);
}
if (!$this->getDatabase()->schema()->fieldExists($this->mapTableName, static::SOURCE_IDS_HASH)) {
$this->getDatabase()->schema()->addField($this->mapTableName, static::SOURCE_IDS_HASH, array(
'type' => 'varchar',
'length' => '64',
'not null' => TRUE,
'description' => 'Hash of source ids. Used as primary key',
));
}
}
}
/**
* Creates schema from an ID definition.
*
* @param array $id_definition
* The definition of the field having the structure as the items returned by
* MigrateSourceInterface or MigrateDestinationInterface::getIds().
*
* @return array
* The database schema definition.
*
* @see \Drupal\migrate\Plugin\MigrateSourceInterface::getIds()
* @see \Drupal\migrate\Plugin\MigrateDestinationInterface::getIds()
*/
protected function getFieldSchema(array $id_definition) {
$type_parts = explode('.', $id_definition['type']);
if (count($type_parts) == 1) {
$type_parts[] = 'value';
}
unset($id_definition['type']);
// Get the field storage definition.
$definition = BaseFieldDefinition::create($type_parts[0]);
// Get a list of setting keys belonging strictly to the field definition.
$default_field_settings = $definition->getSettings();
// Separate field definition settings from custom settings. Custom settings
// are settings passed in $id_definition that are not part of field storage
// definition settings.
$field_settings = array_intersect_key($id_definition, $default_field_settings);
$custom_settings = array_diff_key($id_definition, $default_field_settings);
// Resolve schema from field storage definition settings.
$schema = $definition
->setSettings($field_settings)
->getColumns()[$type_parts[1]];
// Merge back custom settings.
return $schema + $custom_settings;
}
/**
* {@inheritdoc}
*/
public function getRowBySource(array $source_id_values) {
$query = $this->getDatabase()->select($this->mapTableName(), 'map')
->fields('map');
$query->condition(static::SOURCE_IDS_HASH, $this->getSourceIDsHash($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 $field_name => $destination_id) {
$query->condition("map.$destination_id", $destination_id_values[$field_name], '=');
}
$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_values) {
$source_id_fields = $this->sourceIdFields();
$query = $this->getDatabase()->select($this->mapTableName(), 'map');
foreach ($source_id_fields as $source_field_name => $idmap_field_name) {
$query->addField('map', $idmap_field_name, $source_field_name);
}
foreach ($this->destinationIdFields() as $field_name => $destination_id) {
$query->condition("map.$destination_id", $destination_id_values[$field_name], '=');
}
$result = $query->execute();
return $result->fetchAssoc() ?: [];
}
/**
* {@inheritdoc}
*/
public function lookupDestinationId(array $source_id_values) {
$results = $this->lookupDestinationIds($source_id_values);
return $results ? reset($results) : array();
}
/**
* {@inheritdoc}
*/
public function lookupDestinationIds(array $source_id_values) {
if (empty($source_id_values)) {
return array();
}
// Canonicalize the keys into a hash of DB-field => value.
$is_associative = !isset($source_id_values[0]);
$conditions = [];
foreach ($this->sourceIdFields() as $field_name => $db_field) {
if ($is_associative) {
// Associative $source_id_values can have fields out of order.
if (isset($source_id_values[$field_name])) {
$conditions[$db_field] = $source_id_values[$field_name];
unset($source_id_values[$field_name]);
}
}
else {
// For non-associative $source_id_values, we assume they're the first
// few fields.
if (empty($source_id_values)) {
break;
}
$conditions[$db_field] = array_shift($source_id_values);
}
}
if (!empty($source_id_values)) {
throw new MigrateException("Extra unknown items in source IDs");
}
$query = $this->getDatabase()->select($this->mapTableName(), 'map')
->fields('map', $this->destinationIdFields());
if (count($this->sourceIdFields()) === count($conditions)) {
// Optimization: Use the primary key.
$query->condition(self::SOURCE_IDS_HASH, $this->getSourceIDsHash(array_values($conditions)));
}
else {
foreach ($conditions as $db_field => $value) {
$query->condition($db_field, $value);
}
}
return $query->execute()->fetchAll(\PDO::FETCH_NUM);
}
/**
* {@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.
$fields = [];
foreach ($this->sourceIdFields() as $field_name => $key_name) {
// A NULL key value is usually an indication of a problem.
if (!isset($source_id_values[$field_name])) {
$this->message->display($this->t(
'Did not save to map table due to NULL value for key field @field',
array('@field' => $field_name)), 'error');
return;
}
$fields[$key_name] = $source_id_values[$field_name];
}
if (!$fields) {
return;
}
$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->getTrackLastImported()) {
$fields['last_imported'] = time();
}
$keys = [static::SOURCE_IDS_HASH => $this->getSourceIDsHash($source_id_values)];
// Notify anyone listening of the map row we're about to save.
$this->eventDispatcher->dispatch(MigrateEvents::MAP_SAVE, new MigrateMapSaveEvent($this, $fields));
$this->getDatabase()->merge($this->mapTableName())
->key($keys)
->fields($fields)
->execute();
}
/**
* {@inheritdoc}
*/
public function saveMessage(array $source_id_values, $message, $level = MigrationInterface::MESSAGE_ERROR) {
foreach ($this->sourceIdFields() as $field_name => $source_id) {
// If any key value is not set, we can't save.
if (!isset($source_id_values[$field_name])) {
return;
}
}
$fields[static::SOURCE_IDS_HASH] = $this->getSourceIDsHash($source_id_values);
$fields['level'] = $level;
$fields['message'] = $message;
$this->getDatabase()->insert($this->messageTableName())
->fields($fields)
->execute();
// Notify anyone listening of the message we've saved.
$this->eventDispatcher->dispatch(MigrateEvents::IDMAP_MESSAGE,
new MigrateIdMapMessageEvent($this->migration, $source_id_values, $message, $level));
}
/**
* {@inheritdoc}
*/
public function getMessageIterator(array $source_id_values = [], $level = NULL) {
$query = $this->getDatabase()->select($this->messageTableName(), 'msg')
->fields('msg');
if ($source_id_values) {
$query->condition(static::SOURCE_IDS_HASH, $this->getSourceIDsHash($source_id_values));
}
if ($level) {
$query->condition('level', $level);
}
return $query->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 int $status
* An integer for the source_row_status column.
* @param string $table
* (optional) The table to work. Defaults to NULL.
*
* @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());
$map_query->condition(static::SOURCE_IDS_HASH, $this->getSourceIDsHash($source_id_values));
// Notify anyone listening of the map row we're about to delete.
$this->eventDispatcher->dispatch(MigrateEvents::MAP_DELETE, new MigrateMapDeleteEvent($this, $source_id_values));
$map_query->execute();
}
$message_query = $this->getDatabase()->delete($this->messageTableName());
$message_query->condition(static::SOURCE_IDS_HASH, $this->getSourceIDsHash($source_id_values));
$message_query->execute();
}
/**
* {@inheritdoc}
*/
public function deleteDestination(array $destination_id_values) {
$map_query = $this->getDatabase()->delete($this->mapTableName());
$message_query = $this->getDatabase()->delete($this->messageTableName());
$source_id_values = $this->lookupSourceID($destination_id_values);
if (!empty($source_id_values)) {
foreach ($this->destinationIdFields() as $field_name => $destination_id) {
$map_query->condition($destination_id, $destination_id_values[$field_name]);
}
// Notify anyone listening of the map row we're about to delete.
$this->eventDispatcher->dispatch(MigrateEvents::MAP_DELETE, new MigrateMapDeleteEvent($this, $source_id_values));
$map_query->execute();
$message_query->condition(static::SOURCE_IDS_HASH, $this->getSourceIDsHash($source_id_values));
$message_query->execute();
}
}
/**
* {@inheritdoc}
*/
public function setUpdate(array $source_id_values) {
if (empty($source_id_values)) {
throw new MigrateException('No source identifiers provided to update.');
}
$query = $this->getDatabase()
->update($this->mapTableName())
->fields(array('source_row_status' => MigrateIdMapInterface::STATUS_NEEDS_UPDATE));
foreach ($this->sourceIdFields() as $field_name => $source_id) {
$query->condition($source_id, $source_id_values[$field_name]);
}
$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.
*/
public function rewind() {
$this->currentRow = NULL;
$fields = array();
foreach ($this->sourceIdFields() as $field) {
$fields[] = $field;
}
foreach ($this->destinationIdFields() as $field) {
$fields[] = $field;
}
$this->result = $this->getDatabase()->select($this->mapTableName(), 'map')
->fields('map', $fields)
->orderBy('destid1')
->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);
}
/**
* {@inheritdoc}
*/
public function currentDestination() {
if ($this->valid()) {
$result = array();
foreach ($this->destinationIdFields() as $destination_field_name => $idmap_field_name) {
if (!is_null($this->currentRow[$idmap_field_name])) {
$result[$destination_field_name] = $this->currentRow[$idmap_field_name];
}
}
return $result;
}
else {
return NULL;
}
}
/**
* @inheritdoc
*/
public function currentSource() {
if ($this->valid()) {
$result = array();
foreach ($this->sourceIdFields() as $field_name => $source_id) {
$result[$field_name] = $this->currentKey[$source_id];
}
return $result;
}
else {
return NULL;
}
}
/**
* 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() {
return $this->currentRow !== FALSE;
}
}

View file

@ -0,0 +1,107 @@
<?php
namespace Drupal\migrate\Plugin\migrate\process;
use Drupal\migrate\MigrateException;
use Drupal\migrate\MigrateExecutableInterface;
use Drupal\migrate\ProcessPluginBase;
use Drupal\migrate\Row;
/**
* Builds an array based on the key and value configuration.
*
* The array_build plugin builds a single associative array by extracting keys
* and values from each array in the input value, which is expected to be an
* array of arrays. The keys of the returned array will be determined by the
* 'key' configuration option, and the values will be determined by the 'value'
* option.
*
* Available configuration keys
* - key: The key used to lookup a value in the source arrays to be used as
* a key in the destination array.
* - value: The key used to lookup a value in the source arrays to be used as
* a value in the destination array.
*
* Example:
*
* Consider the migration of language negotiation by domain.
* The source is an array of all the languages:
*
* @code
* languages: Array
* (
* [0] => Array
* (
* [language] => en
* ...
* [domain] => http://example.com
* )
* [1] => Array
* (
* [language] => fr
* ...
* [domain] => http://fr.example.com
* )
* ...
* @endcode
*
* The destination should be an array of all the domains keyed by their
* language code:
*
* @code
* domains: Array
* (
* [en] => http://example.com
* [fr] => http://fr.example.com
* ...
* @endcode
*
* The array_build process plugin would be used like this:
*
* @code
* process:
* domains:
* plugin: array_build
* key: language
* value: domain
* source: languages
* @endcode
*
* @see \Drupal\migrate\Plugin\MigrateProcessInterface
*
* @MigrateProcessPlugin(
* id = "array_build",
* handle_multiples = TRUE
* )
*/
class ArrayBuild extends ProcessPluginBase {
/**
* {@inheritdoc}
*/
public function transform($value, MigrateExecutableInterface $migrate_executable, Row $row, $destination_property) {
$new_value = [];
foreach ((array) $value as $old_key => $old_value) {
// Checks that $old_value is an array.
if (!is_array($old_value)) {
throw new MigrateException("The input should be an array of arrays");
}
// Checks that the key exists.
if (!array_key_exists($this->configuration['key'], $old_value)) {
throw new MigrateException("The key '" . $this->configuration['key'] . "' does not exist");
}
// Checks that the value exists.
if (!array_key_exists($this->configuration['value'], $old_value)) {
throw new MigrateException("The key '" . $this->configuration['value'] . "' does not exist");
}
$new_value[$old_value[$this->configuration['key']]] = $old_value[$this->configuration['value']];
}
return $new_value;
}
}

View file

@ -0,0 +1,36 @@
<?php
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.
*
* @link https://www.drupal.org/node/2181783 Online handbook documentation for callback process plugin @endlink
*
* @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,37 @@
<?php
namespace Drupal\migrate\Plugin\migrate\process;
use Drupal\migrate\MigrateException;
use Drupal\migrate\MigrateExecutableInterface;
use Drupal\migrate\ProcessPluginBase;
use Drupal\migrate\Row;
/**
* Concatenates the strings in the current value.
*
* @link https://www.drupal.org/node/2345927 Online handbook documentation for concat process plugin @endlink
*
* @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', var_export($value, TRUE)));
}
}
}

View file

@ -0,0 +1,57 @@
<?php
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.
*
* @link https://www.drupal.org/node/2345929 Online handbook documentation for dedupebase process plugin @endlink
*/
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,86 @@
<?php
namespace Drupal\migrate\Plugin\migrate\process;
use Drupal\Core\Entity\Query\QueryFactory;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Drupal\migrate\Plugin\MigrationInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Ensures value is not duplicated against an entity field.
*
* If the 'migrated' configuration value is true, an entity will only be
* considered a duplicate if it was migrated by the current migration.
*
* @link https://www.drupal.org/node/2135325 Online handbook documentation for dedupe_entity process plugin @endlink
*
* @MigrateProcessPlugin(
* id = "dedupe_entity"
* )
*/
class DedupeEntity extends DedupeBase implements ContainerFactoryPluginInterface {
/**
* The entity query factory.
*
* @var \Drupal\Core\Entity\Query\QueryFactoryInterface
*/
protected $entityQueryFactory;
/**
* The current migration.
*
* @var \Drupal\migrate\Plugin\MigrationInterface
*/
protected $migration;
/**
* {@inheritdoc}
*/
public function __construct(array $configuration, $plugin_id, $plugin_definition, MigrationInterface $migration, QueryFactory $entity_query_factory) {
parent::__construct($configuration, $plugin_id, $plugin_definition);
$this->migration = $migration;
$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.
$query = $this
->entityQueryFactory
->get($this->configuration['entity_type'], 'AND')
->condition($this->configuration['field'], $value);
if (!empty($this->configuration['migrated'])) {
// Check if each entity is in the ID map.
$idMap = $this->migration->getIdMap();
foreach ($query->execute() as $id) {
$dest_id_values[$this->configuration['field']] = $id;
if ($idMap->lookupSourceID($dest_id_values)) {
return TRUE;
}
}
return FALSE;
}
else {
// Just check if any such entity exists.
return $query->count()->execute();
}
}
}

View file

@ -0,0 +1,30 @@
<?php
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.
*
* @link https://www.drupal.org/node/2135313 Online handbook documentation for default_value process plugin @endlink
*
* @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,122 @@
<?php
namespace Drupal\migrate\Plugin\migrate\process;
use Drupal\Core\File\FileSystemInterface;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Drupal\migrate\MigrateException;
use Drupal\migrate\MigrateExecutableInterface;
use Drupal\migrate\ProcessPluginBase;
use Drupal\migrate\Row;
use GuzzleHttp\Client;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Downloads a file from a remote location into the local file system.
*
* @MigrateProcessPlugin(
* id = "download"
* )
*/
class Download extends ProcessPluginBase implements ContainerFactoryPluginInterface {
/**
* The file system service.
*
* @var \Drupal\Core\File\FileSystemInterface
*/
protected $fileSystem;
/**
* The Guzzle HTTP Client service.
*
* @var \GuzzleHttp\Client
*/
protected $httpClient;
/**
* Constructs a download process plugin.
*
* @param array $configuration
* The plugin configuration.
* @param string $plugin_id
* The plugin ID.
* @param mixed $plugin_definition
* The plugin definition.
* @param \Drupal\Core\File\FileSystemInterface $file_system
* The file system service.
* @param \GuzzleHttp\Client $http_client
* The HTTP client.
*/
public function __construct(array $configuration, $plugin_id, array $plugin_definition, FileSystemInterface $file_system, Client $http_client) {
$configuration += [
'rename' => FALSE,
'guzzle_options' => [],
];
parent::__construct($configuration, $plugin_id, $plugin_definition);
$this->fileSystem = $file_system;
$this->httpClient = $http_client;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
return new static(
$configuration,
$plugin_id,
$plugin_definition,
$container->get('file_system'),
$container->get('http_client')
);
}
/**
* {@inheritdoc}
*/
public function transform($value, MigrateExecutableInterface $migrate_executable, Row $row, $destination_property) {
// If we're stubbing a file entity, return a uri of NULL so it will get
// stubbed by the general process.
if ($row->isStub()) {
return NULL;
}
list($source, $destination) = $value;
// Modify the destination filename if necessary.
$replace = !empty($this->configuration['rename']) ?
FILE_EXISTS_RENAME :
FILE_EXISTS_REPLACE;
$final_destination = file_destination($destination, $replace);
// Try opening the file first, to avoid calling file_prepare_directory()
// unnecessarily. We're suppressing fopen() errors because we want to try
// to prepare the directory before we give up and fail.
$destination_stream = @fopen($final_destination, 'w');
if (!$destination_stream) {
// If fopen didn't work, make sure there's a writable directory in place.
$dir = $this->fileSystem->dirname($final_destination);
if (!file_prepare_directory($dir, FILE_CREATE_DIRECTORY | FILE_MODIFY_PERMISSIONS)) {
throw new MigrateException("Could not create or write to directory '$dir'");
}
// Let's try that fopen again.
$destination_stream = @fopen($final_destination, 'w');
if (!$destination_stream) {
throw new MigrateException("Could not write to file '$final_destination'");
}
}
// Stream the request body directly to the final destination stream.
$this->configuration['guzzle_options']['sink'] = $destination_stream;
try {
// Make the request. Guzzle throws an exception for anything but 200.
$this->httpClient->get($source, $this->configuration['guzzle_options']);
}
catch (\Exception $e) {
throw new MigrateException("{$e->getMessage()} ($source)");
}
return $final_destination;
}
}

View file

@ -0,0 +1,46 @@
<?php
namespace Drupal\migrate\Plugin\migrate\process;
use Drupal\migrate\ProcessPluginBase;
use Drupal\migrate\MigrateException;
use Drupal\migrate\MigrateExecutableInterface;
use Drupal\migrate\Row;
/**
* This plugin explodes a delimited string into an array of values.
*
* @link https://www.drupal.org/node/2674504 Online handbook documentation for explode process plugin @endlink
*
* @MigrateProcessPlugin(
* id = "explode"
* )
*/
class Explode extends ProcessPluginBase {
/**
* {@inheritdoc}
*/
public function transform($value, MigrateExecutableInterface $migrate_executable, Row $row, $destination_property) {
if (is_string($value)) {
if (!empty($this->configuration['delimiter'])) {
$limit = isset($this->configuration['limit']) ? $this->configuration['limit'] : PHP_INT_MAX;
return explode($this->configuration['delimiter'], $value, $limit);
}
else {
throw new MigrateException('delimiter is empty');
}
}
else {
throw new MigrateException(sprintf('%s is not a string', var_export($value, TRUE)));
}
}
/**
* {@inheritdoc}
*/
public function multiple() {
return TRUE;
}
}

View file

@ -0,0 +1,42 @@
<?php
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.
*
* @link https://www.drupal.org/node/2152731 Online handbook documentation for extract process plugin @endlink
*
* @MigrateProcessPlugin(
* id = "extract",
* handle_multiples = TRUE
* )
*/
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) {
if (isset($this->configuration['default'])) {
$new_value = $this->configuration['default'];
}
else {
throw new MigrateException('Array index missing, extraction failed.');
}
}
return $new_value;
}
}

View file

@ -0,0 +1,236 @@
<?php
namespace Drupal\migrate\Plugin\migrate\process;
use Drupal\Core\File\FileSystemInterface;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Drupal\Core\StreamWrapper\LocalStream;
use Drupal\Core\StreamWrapper\StreamWrapperManagerInterface;
use Drupal\migrate\MigrateException;
use Drupal\migrate\MigrateExecutableInterface;
use Drupal\migrate\Plugin\MigrateProcessInterface;
use Drupal\migrate\ProcessPluginBase;
use Drupal\migrate\Row;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Copy a file from one place into another.
*
* @MigrateProcessPlugin(
* id = "file_copy"
* )
*/
class FileCopy extends ProcessPluginBase implements ContainerFactoryPluginInterface {
/**
* The stream wrapper manager service.
*
* @var \Drupal\Core\StreamWrapper\StreamWrapperManagerInterface
*/
protected $streamWrapperManager;
/**
* The file system service.
*
* @var \Drupal\Core\File\FileSystemInterface
*/
protected $fileSystem;
/**
* An instance of the download process plugin.
*
* @var \Drupal\migrate\Plugin\MigrateProcessInterface
*/
protected $downloadPlugin;
/**
* Constructs a file_copy process plugin.
*
* @param array $configuration
* The plugin configuration.
* @param string $plugin_id
* The plugin ID.
* @param mixed $plugin_definition
* The plugin definition.
* @param \Drupal\Core\StreamWrapper\StreamWrapperManagerInterface $stream_wrappers
* The stream wrapper manager service.
* @param \Drupal\Core\File\FileSystemInterface $file_system
* The file system service.
* @param \Drupal\migrate\Plugin\MigrateProcessInterface $download_plugin
* An instance of the download plugin for handling remote URIs.
*/
public function __construct(array $configuration, $plugin_id, array $plugin_definition, StreamWrapperManagerInterface $stream_wrappers, FileSystemInterface $file_system, MigrateProcessInterface $download_plugin) {
$configuration += array(
'move' => FALSE,
'rename' => FALSE,
'reuse' => FALSE,
);
parent::__construct($configuration, $plugin_id, $plugin_definition);
$this->streamWrapperManager = $stream_wrappers;
$this->fileSystem = $file_system;
$this->downloadPlugin = $download_plugin;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
return new static(
$configuration,
$plugin_id,
$plugin_definition,
$container->get('stream_wrapper_manager'),
$container->get('file_system'),
$container->get('plugin.manager.migrate.process')->createInstance('download')
);
}
/**
* {@inheritdoc}
*/
public function transform($value, MigrateExecutableInterface $migrate_executable, Row $row, $destination_property) {
// If we're stubbing a file entity, return a URI of NULL so it will get
// stubbed by the general process.
if ($row->isStub()) {
return NULL;
}
list($source, $destination) = $value;
// If the source path or URI represents a remote resource, delegate to the
// download plugin.
if (!$this->isLocalUri($source)) {
return $this->downloadPlugin->transform($value, $migrate_executable, $row, $destination_property);
}
// Ensure the source file exists, if it's a local URI or path.
if (!file_exists($source)) {
throw new MigrateException("File '$source' does not exist");
}
// If the start and end file is exactly the same, there is nothing to do.
if ($this->isLocationUnchanged($source, $destination)) {
return $destination;
}
// Check if a writable directory exists, and if not try to create it.
$dir = $this->getDirectory($destination);
// If the directory exists and is writable, avoid file_prepare_directory()
// call and write the file to destination.
if (!is_dir($dir) || !is_writable($dir)) {
if (!file_prepare_directory($dir, FILE_CREATE_DIRECTORY | FILE_MODIFY_PERMISSIONS)) {
throw new MigrateException("Could not create or write to directory '$dir'");
}
}
$final_destination = $this->writeFile($source, $destination, $this->getOverwriteMode());
if ($final_destination) {
return $final_destination;
}
throw new MigrateException("File $source could not be copied to $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 int $replace
* (optional) FILE_EXISTS_REPLACE (default) or FILE_EXISTS_RENAME.
*
* @return string|bool
* File destination on success, FALSE on failure.
*/
protected function writeFile($source, $destination, $replace = FILE_EXISTS_REPLACE) {
// Check if there is a destination available for copying. If there isn't,
// it already exists at the destination and the replace flag tells us to not
// replace it. In that case, return the original destination.
if (!($final_destination = file_destination($destination, $replace))) {
return $destination;
}
$function = 'file_unmanaged_' . ($this->configuration['move'] ? 'move' : 'copy');
return $function($source, $destination, $replace);
}
/**
* Determines how to handle file conflicts.
*
* @return int
* FILE_EXISTS_REPLACE (default), FILE_EXISTS_RENAME, or FILE_EXISTS_ERROR
* depending on the current configuration.
*/
protected function getOverwriteMode() {
if (!empty($this->configuration['rename'])) {
return FILE_EXISTS_RENAME;
}
if (!empty($this->configuration['reuse'])) {
return FILE_EXISTS_ERROR;
}
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 string|false
* 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);
}
return $dir;
}
/**
* Determines if the source and destination URIs represent identical paths.
*
* @param string $source
* The source URI.
* @param string $destination
* The destination URI.
*
* @return bool
* TRUE if the source and destination URIs refer to the same physical path,
* otherwise FALSE.
*/
protected function isLocationUnchanged($source, $destination) {
return $this->fileSystem->realpath($source) === $this->fileSystem->realpath($destination);
}
/**
* Determines 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 bool
*/
protected function isLocalUri($uri) {
$scheme = $this->fileSystem->uriScheme($uri);
// The vfs scheme is vfsStream, which is used in testing. vfsStream is a
// simulated file system that exists only in memory, but should be treated
// as a local resource.
if ($scheme == 'vfs') {
$scheme = FALSE;
}
return $scheme === FALSE || $this->streamWrapperManager->getViaScheme($scheme) instanceof LocalStream;
}
}

View file

@ -0,0 +1,33 @@
<?php
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.
*
* @link https://www.drupal.org/node/2154215 Online handbook documentation for flatten process plugin @endlink
*
* @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,73 @@
<?php
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.
*
* @link https://www.drupal.org/node/2135307 Online handbook documentation for get process plugin @endlink
*
* @MigrateProcessPlugin(
* id = "get"
* )
*/
class Get extends ProcessPluginBase {
/**
* Flag indicating whether there are multiple values.
*
* @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 ($property || (string) $property === '0') {
$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);
}
}
else {
$return[] = $value;
}
}
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,66 @@
<?php
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.
*
* @link https://www.drupal.org/node/2135345 Online handbook documentation for iterator process plugin @endlink
*
* @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 = [];
if (!is_null($value)) {
foreach ($value as $key => $new_value) {
$new_row = new Row($new_value, []);
$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,74 @@
<?php
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.
*
* @link https://www.drupal.org/node/2135323 Online handbook documentation for machine_name process plugin @endlink
*
* @MigrateProcessPlugin(
* id = "machine_name"
* )
*/
class MachineName extends ProcessPluginBase implements ContainerFactoryPluginInterface {
/**
* The transliteration service.
*
* @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,103 @@
<?php
namespace Drupal\migrate\Plugin\migrate\process;
use Drupal\Core\Entity\EntityStorageInterface;
use Drupal\Core\Menu\MenuLinkManagerInterface;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Drupal\Core\Url;
use Drupal\migrate\Plugin\MigrationInterface;
use Drupal\migrate\MigrateExecutableInterface;
use Drupal\migrate\MigrateSkipRowException;
use Drupal\migrate\Plugin\MigrateProcessInterface;
use Drupal\migrate\ProcessPluginBase;
use Drupal\migrate\Row;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* This plugin figures out menu link parent plugin IDs.
*
* @MigrateProcessPlugin(
* id = "menu_link_parent"
* )
*/
class MenuLinkParent extends ProcessPluginBase implements ContainerFactoryPluginInterface {
/**
* @var \Drupal\Core\Menu\MenuLinkManagerInterface
*/
protected $menuLinkManager;
/**
* @var \Drupal\migrate\Plugin\MigrateProcessInterface
*/
protected $migrationPlugin;
/**
* @var \Drupal\Core\Entity\EntityStorageInterface
*/
protected $menuLinkStorage;
/**
* {@inheritdoc}
*/
public function __construct(array $configuration, $plugin_id, $plugin_definition, MigrateProcessInterface $migration_plugin, MenuLinkManagerInterface $menu_link_manager, EntityStorageInterface $menu_link_storage) {
parent::__construct($configuration, $plugin_id, $plugin_definition);
$this->migrationPlugin = $migration_plugin;
$this->menuLinkManager = $menu_link_manager;
$this->menuLinkStorage = $menu_link_storage;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition, MigrationInterface $migration = NULL) {
$migration_configuration['migration'][] = $migration->id();
return new static(
$configuration,
$plugin_id,
$plugin_definition,
$container->get('plugin.manager.migrate.process')->createInstance('migration', $migration_configuration, $migration),
$container->get('plugin.manager.menu.link'),
$container->get('entity.manager')->getStorage('menu_link_content')
);
}
/**
* {@inheritdoc}
*
* Find the parent link GUID.
*/
public function transform($value, MigrateExecutableInterface $migrate_executable, Row $row, $destination_property) {
$parent_id = array_shift($value);
if (!$parent_id) {
// Top level item.
return '';
}
try {
$already_migrated_id = $this
->migrationPlugin
->transform($parent_id, $migrate_executable, $row, $destination_property);
if ($already_migrated_id && ($link = $this->menuLinkStorage->load($already_migrated_id))) {
return $link->getPluginId();
}
}
catch (MigrateSkipRowException $e) {
}
if (isset($value[1])) {
list($menu_name, $parent_link_path) = $value;
$url = Url::fromUserInput("/$parent_link_path");
if ($url->isRouted()) {
$links = $this->menuLinkManager->loadLinksByRoute($url->getRouteName(), $url->getRouteParameters(), $menu_name);
if (count($links) == 1) {
/** @var \Drupal\Core\Menu\MenuLinkInterface $link */
$link = reset($links);
return $link->getPluginId();
}
}
}
throw new MigrateSkipRowException();
}
}

View file

@ -0,0 +1,176 @@
<?php
namespace Drupal\migrate\Plugin\migrate\process;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Drupal\migrate\MigrateSkipProcessException;
use Drupal\migrate\Plugin\MigratePluginManagerInterface;
use Drupal\migrate\Plugin\MigrationPluginManagerInterface;
use Drupal\migrate\Plugin\MigrateIdMapInterface;
use Drupal\migrate\ProcessPluginBase;
use Drupal\migrate\Plugin\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.
*
* @link https://www.drupal.org/node/2149801 Online handbook documentation for migration process plugin @endlink
*
* @MigrateProcessPlugin(
* id = "migration"
* )
*/
class Migration extends ProcessPluginBase implements ContainerFactoryPluginInterface {
/**
* The process plugin manager.
*
* @var \Drupal\migrate\Plugin\MigratePluginManager
*/
protected $processPluginManager;
/**
* The migration plugin manager.
*
* @var \Drupal\migrate\Plugin\MigrationPluginManagerInterface
*/
protected $migrationPluginManager;
/**
* The migration to be executed.
*
* @var \Drupal\migrate\Plugin\MigrationInterface
*/
protected $migration;
/**
* {@inheritdoc}
*/
public function __construct(array $configuration, $plugin_id, $plugin_definition, MigrationInterface $migration, MigrationPluginManagerInterface $migration_plugin_manager, MigratePluginManagerInterface $process_plugin_manager) {
parent::__construct($configuration, $plugin_id, $plugin_definition);
$this->migrationPluginManager = $migration_plugin_manager;
$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('plugin.manager.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);
}
if (!is_array($value)) {
$value = array($value);
}
$this->skipOnEmpty($value);
$self = FALSE;
/** @var \Drupal\migrate\Plugin\MigrationInterface[] $migrations */
$destination_ids = NULL;
$source_id_values = array();
$migrations = $this->migrationPluginManager->createInstances($migration_ids);
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 && !empty($this->configuration['no_stub'])) {
return NULL;
}
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->getProcess();
// 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->getSourceConfiguration(), $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) {
$migration->getIdMap()->saveMessage($stub_row->getSourceIdValues(), $e->getMessage());
}
if ($destination_ids) {
$migration->getIdMap()->saveIdMapping($stub_row, $destination_ids, MigrateIdMapInterface::STATUS_NEEDS_UPDATE);
}
}
if ($destination_ids) {
if (count($destination_ids) == 1) {
return reset($destination_ids);
}
else {
return $destination_ids;
}
}
}
/**
* Skips the migration process entirely if the value is FALSE.
*
* @param mixed $value
* The incoming value to transform.
*
* @throws \Drupal\migrate\MigrateSkipProcessException
*/
protected function skipOnEmpty(array $value) {
if (!array_filter($value)) {
throw new MigrateSkipProcessException();
}
}
}

View file

@ -0,0 +1,101 @@
<?php
namespace Drupal\migrate\Plugin\migrate\process;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Drupal\Core\Path\PathValidatorInterface;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Drupal\migrate\MigrateExecutableInterface;
use Drupal\migrate\Plugin\MigrationInterface;
use Drupal\migrate\ProcessPluginBase;
use Drupal\migrate\Row;
/**
*
* @link https://www.drupal.org/node/2750777 Online handbook documentation for route process plugin @endlink
*
* * @MigrateProcessPlugin(
* id = "route"
* )
*/
class Route extends ProcessPluginBase implements ContainerFactoryPluginInterface {
/**
* The path validator service.
*
* @var \Drupal\Core\Path\PathValidatorInterface
*/
protected $pathValidator;
/**
* {@inheritdoc}
*/
public function __construct(array $configuration, $plugin_id, $plugin_definition, MigrationInterface $migration, PathValidatorInterface $path_validator) {
parent::__construct($configuration, $plugin_id, $plugin_definition);
$this->migration = $migration;
$this->pathValidator = $path_validator;
}
/**
* {@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) {
if (is_string($value)) {
$link_path = $value;
$options = [];
}
else {
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,42 @@
<?php
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.
*
* @link https://www.drupal.org/node/2228793 Online handbook documentation for skip_on_empty process plugin @endlink
*
* @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,32 @@
<?php
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.
*
* @link https://www.drupal.org/node/2345935 Online handbook documentation for skip_row_if_not_set process plugin @endlink
*
* @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,54 @@
<?php
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.
*
* @link https://www.drupal.org/node/2143521 Online handbook documentation for static_map process plugin @endlink
*
* @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 (array_key_exists('default_value', $this->configuration)) {
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,43 @@
<?php
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 plugin returns a substring of the current value.
*
* @link https://www.drupal.org/node/2771965 Online handbook documentation for substr process plugin @endlink
*
* @MigrateProcessPlugin(
* id = "substr"
* )
*/
class Substr extends ProcessPluginBase {
/**
* {@inheritdoc}
*/
public function transform($value, MigrateExecutableInterface $migrate_executable, Row $row, $destination_property) {
$start = isset($this->configuration['start']) ? $this->configuration['start'] : 0;
if (!is_int($start)) {
throw new MigrateException('The start position configuration value 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 value should be an integer. Omit this key to capture from the start position to the end of the string.');
}
if (!is_string($value)) {
throw new MigrateException('The input value must be a string.');
}
// Use optional start or length to return a portion of $value.
$new_value = Unicode::substr($value, $start, $length);
return $new_value;
}
}

View file

@ -0,0 +1,60 @@
<?php
namespace Drupal\migrate\Plugin\migrate\process;
use Drupal\migrate\MigrateExecutableInterface;
use Drupal\migrate\MigrateException;
use Drupal\migrate\ProcessPluginBase;
use Drupal\migrate\Row;
use GuzzleHttp\Psr7\Uri;
/**
* Apply urlencoding to a URI.
*
* This is needed when the URI is to be opened by a later migration stage, and
* the source URI value is not already encoded.
*
* @MigrateProcessPlugin(
* id = "urlencode"
* )
*/
class UrlEncode extends ProcessPluginBase {
/**
* {@inheritdoc}
*/
public function transform($value, MigrateExecutableInterface $migrate_executable, Row $row, $destination_property) {
// Only apply to a full URL.
if (is_string($value) && strpos($value, '://') > 0) {
// URL encode everything after the hostname.
$parsed_url = parse_url($value);
// Fail on seriously malformed URLs.
if ($parsed_url === FALSE) {
throw new MigrateException("Value '$value' is not a valid URL");
}
// Iterate over specific pieces of the URL rawurlencoding each one.
$url_parts_to_encode = array('path', 'query', 'fragment');
foreach ($parsed_url as $parsed_url_key => $parsed_url_value) {
if (in_array($parsed_url_key, $url_parts_to_encode)) {
// urlencode() would convert spaces to + signs.
$urlencoded_parsed_url_value = rawurlencode($parsed_url_value);
// Restore special characters depending on which part of the URL this is.
switch ($parsed_url_key) {
case 'query':
$urlencoded_parsed_url_value = str_replace('%26', '&', $urlencoded_parsed_url_value);
break;
case 'path':
$urlencoded_parsed_url_value = str_replace('%2F', '/', $urlencoded_parsed_url_value);
break;
}
$parsed_url[$parsed_url_key] = $urlencoded_parsed_url_value;
}
}
$value = (string) Uri::fromParts($parsed_url);
}
return $value;
}
}

View file

@ -0,0 +1,34 @@
<?php
namespace Drupal\migrate\Plugin\migrate\source;
/**
* Provides a dummy select query object for source plugins.
*
* Trait providing a dummy select query object for source plugins based on
* SqlBase which override initializeIterator() to obtain their data from other
* SqlBase services instead of a direct query. This ensures that query() returns
* a valid object, even though it is not used for iteration.
*/
trait DummyQueryTrait {
/**
* {@inheritdoc}
*/
public function query() {
// Pass an arbritrary table name - the query should never be executed
// anyway.
$query = $this->select(uniqid(), 's')
->range(0, 1);
$query->addExpression('1');
return $query;
}
/**
* {@inheritdoc}
*/
public function count() {
return 1;
}
}

View file

@ -0,0 +1,84 @@
<?php
namespace Drupal\migrate\Plugin\migrate\source;
use Drupal\migrate\Plugin\MigrationInterface;
/**
* Source which takes its data directly from the plugin config.
*
* @MigrateSource(
* id = "embedded_data"
* )
*/
class EmbeddedDataSource extends SourcePluginBase {
/**
* Data obtained from the source plugin configuration.
*
* @var array[]
* Array of data rows, each one an array of values keyed by field names.
*/
protected $dataRows = [];
/**
* Description of the unique ID fields for this source.
*
* @var array[]
* Each array member is keyed by a field name, with a value that is an
* array with a single member with key 'type' and value a column type such
* as 'integer'.
*/
protected $ids = [];
/**
* {@inheritdoc}
*/
public function __construct(array $configuration, $plugin_id, $plugin_definition, MigrationInterface $migration) {
parent::__construct($configuration, $plugin_id, $plugin_definition, $migration);
$this->dataRows = $configuration['data_rows'];
$this->ids = $configuration['ids'];
}
/**
* {@inheritdoc}
*/
public function fields() {
if ($this->count() > 0) {
$first_row = reset($this->dataRows);
$field_names = array_keys($first_row);
return array_combine($field_names, $field_names);
}
else {
return [];
}
}
/**
* {@inheritdoc}
*/
public function initializeIterator() {
return new \ArrayIterator($this->dataRows);
}
/**
* {@inheritdoc}
*/
public function __toString() {
return 'Embedded data';
}
/**
* {@inheritdoc}
*/
public function getIds() {
return $this->ids;
}
/**
* {@inheritdoc}
*/
public function count() {
return count($this->dataRows);
}
}

View file

@ -0,0 +1,54 @@
<?php
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' => '')));
}
/**
* Allows class to decide how it will react when it is treated like a string.
*/
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,544 @@
<?php
namespace Drupal\migrate\Plugin\migrate\source;
use Drupal\Core\Plugin\PluginBase;
use Drupal\migrate\Event\MigrateRollbackEvent;
use Drupal\migrate\Event\RollbackAwareInterface;
use Drupal\migrate\Plugin\MigrationInterface;
use Drupal\migrate\MigrateException;
use Drupal\migrate\MigrateSkipRowException;
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, RollbackAwareInterface {
/**
* The module handler service.
*
* @var \Drupal\Core\Extension\ModuleHandlerInterface
*/
protected $moduleHandler;
/**
* The entity migration object.
*
* @var \Drupal\migrate\Plugin\MigrationInterface
*/
protected $migration;
/**
* The current row from the query.
*
* @var \Drupal\Migrate\Row
*/
protected $currentRow;
/**
* The primary key of the current row.
*
* @var array
*/
protected $currentSourceIds;
/**
* Information on the property used as the high-water mark.
*
* Array of 'name' and (optional) db 'alias' properties used for high-water
* mark.
*
* @var array
*/
protected $highWaterProperty = [];
/**
* The key-value storage for the high-water value.
*
* @var \Drupal\Core\KeyValueStore\KeyValueStoreInterface
*/
protected $highWaterStorage;
/**
* The high water mark at the beginning of the import operation.
*
* If the source has a property for tracking changes (like Drupal has
* node.changed) then this is the highest value of those imported so far.
*
* @var int
*/
protected $originalHighWater;
/**
* 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;
/**
* Flags whether to track changes to incoming data.
*
* If TRUE, we will maintain hashed source rows to determine whether incoming
* data has changed.
*
* @var bool
*/
protected $trackChanges = FALSE;
/**
* Flags whether source plugin will read the map row and add to data row.
*
* 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;
/**
* The backend cache.
*
* @var \Drupal\Core\Cache\CacheBackendInterface
*/
protected $cache;
/**
* The migration ID map.
*
* @var \Drupal\migrate\Plugin\MigrateIdMapInterface
*/
protected $idMap;
/**
* The iterator to iterate over the source rows.
*
* @var \Iterator
*/
protected $iterator;
/**
* {@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.
foreach (['cacheCounts' => 'cache_counts', 'skipCount' => 'skip_count', 'trackChanges' => 'track_changes'] as $property => $config_key) {
if (isset($configuration[$config_key])) {
$this->$property = (bool) $configuration[$config_key];
}
}
$this->cacheKey = !empty($configuration['cache_key']) ? $configuration['cache_key'] : NULL;
$this->idMap = $this->migration->getIdMap();
$this->highWaterProperty = !empty($configuration['high_water_property']) ? $configuration['high_water_property'] : FALSE;
// Pull out the current highwater mark if we have a highwater property.
if ($this->highWaterProperty) {
$this->originalHighWater = $this->getHighWater();
}
// 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');
}
}
/**
* Initializes the iterator with the source data.
*
* @return array
* An array of the data for this source.
*/
protected abstract function initializeIterator();
/**
* Gets 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;
try {
$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 will skip if any hook returned FALSE.
$skip = ($result_hook && in_array(FALSE, $result_hook)) || ($result_named_hook && in_array(FALSE, $result_named_hook));
$save_to_map = TRUE;
}
catch (MigrateSkipRowException $e) {
$skip = TRUE;
$save_to_map = $e->getSaveToMap();
if ($message = trim($e->getMessage())) {
$this->idMap->saveMessage($row->getSourceIdValues(), $message, MigrationInterface::MESSAGE_INFORMATIONAL);
}
}
// We're explicitly skipping this row - keep track in the map table.
if ($skip) {
// Make sure we replace any previous messages for this item with any
// new ones.
if ($save_to_map) {
$this->idMap->saveIdMapping($row, array(), MigrateIdMapInterface::STATUS_IGNORED);
$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();
}
return $result;
}
/**
* Returns the iterator that will yield the row arrays to be processed.
*
* @return \Iterator
* The iterator that will yield the row arrays to be processed.
*/
protected function getIterator() {
if (!isset($this->iterator)) {
$this->iterator = $this->initializeIterator();
}
return $this->iterator;
}
/**
* {@inheritdoc}
*/
public function current() {
return $this->currentRow;
}
/**
* Gets 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);
}
/**
* Checks 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);
}
/**
* Rewinds 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->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->fetchNextRow();
$row = new Row($row_data, $this->migration->getSourcePlugin()->getIds(), $this->migration->getDestinationIds());
// Populate the source key for this row.
$this->currentSourceIds = $row->getSourceIdValues();
// Pick up the existing map row, if any, unless fetchNextRow() did it.
if (!$this->mapRowAdded && ($id_map = $this->idMap->getRowBySource($this->currentSourceIds))) {
$row->setIdMap($id_map);
}
// Clear any previous messages for this row before potentially adding
// new ones.
if (!empty($this->currentSourceIds)) {
$this->idMap->delete($this->currentSourceIds, TRUE);
}
// Preparing the row gives source plugins the chance to skip.
if ($this->prepareRow($row) === FALSE) {
continue;
}
// Check whether the row needs processing.
// 1. This row has not been imported yet.
// 2. Explicitly set to update.
// 3. The row is newer than the current highwater mark.
// 4. If no such property exists then try by checking the hash of the row.
if (!$row->getIdMap() || $row->needsUpdate() || $this->aboveHighwater($row) || $this->rowChanged($row)) {
$this->currentRow = $row->freezeSource();
}
if ($this->getHighWaterProperty()) {
$this->saveHighWater($row->getSourceProperty($this->highWaterProperty['name']));
}
}
}
/**
* Position the iterator to the following row.
*/
protected function fetchNextRow() {
$this->getIterator()->next();
}
/**
* 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->getHighWaterProperty() && $row->getSourceProperty($this->highWaterProperty['name']) > $this->originalHighWater;
}
/**
* Checks 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();
}
/**
* Gets the currentSourceIds data member.
*/
public function getCurrentIds() {
return $this->currentSourceIds;
}
/**
* Gets 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
* (optional) Whether or not to refresh the count. Defaults to FALSE.
*
* @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->doCount();
$this->getCache()->set($this->cacheKey, $count);
}
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->doCount();
$this->getCache()->set($this->cacheKey, $count);
}
}
return $count;
}
/**
* Gets 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;
}
/**
* Gets the source count checking if the source is countable or using the
* iterator_count function.
*
* @return int
*/
protected function doCount() {
$iterator = $this->getIterator();
return $iterator instanceof \Countable ? $iterator->count() : iterator_count($this->initializeIterator());
}
/**
* 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;
}
/**
* 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|null
* A Unix timestamp representing the high water mark, or NULL if no high
* water mark has been stored.
*/
protected function getHighWater() {
return $this->getHighWaterStorage()->get($this->migration->id());
}
/**
* Save the new high water mark.
*
* @param int $high_water
* The high water timestamp.
*/
protected function saveHighWater($high_water) {
$this->getHighWaterStorage()->set($this->migration->id(), $high_water);
}
/**
* Get information on the property used as the high watermark.
*
* Array of 'name' & (optional) db 'alias' properties used for high watermark.
*
* @see \Drupal\migrate\Plugin\migrate\source\SqlBase::initializeIterator()
*
* @return array
* The property used as the high watermark.
*/
protected function getHighWaterProperty() {
return $this->highWaterProperty;
}
/**
* Get the name of the field used as the high watermark.
*
* The name of the field qualified with an alias if available.
*
* @see \Drupal\migrate\Plugin\migrate\source\SqlBase::initializeIterator()
*
* @return string|null
* The name of the field for the high water mark, or NULL if not set.
*/
protected function getHighWaterField() {
if (!empty($this->highWaterProperty['name'])) {
return !empty($this->highWaterProperty['alias']) ?
$this->highWaterProperty['alias'] . '.' . $this->highWaterProperty['name'] :
$this->highWaterProperty['name'];
}
return NULL;
}
/**
* {@inheritdoc}
*/
public function preRollback(MigrateRollbackEvent $event) {
// Nothing to do in this implementation.
}
/**
* {@inheritdoc}
*/
public function postRollback(MigrateRollbackEvent $event) {
// Reset the high-water mark.
$this->saveHighWater(NULL);
}
}

View file

@ -0,0 +1,342 @@
<?php
namespace Drupal\migrate\Plugin\migrate\source;
use Drupal\Core\Database\Database;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Drupal\Core\State\StateInterface;
use Drupal\migrate\MigrateException;
use Drupal\migrate\Plugin\MigrationInterface;
use Drupal\migrate\Plugin\migrate\id_map\Sql;
use Drupal\migrate\Plugin\MigrateIdMapInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* 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 implements ContainerFactoryPluginInterface {
/**
* The query string.
*
* @var \Drupal\Core\Database\Query\SelectInterface
*/
protected $query;
/**
* The database object.
*
* @var \Drupal\Core\Database\Connection
*/
protected $database;
/**
* State service for retrieving database info.
*
* @var \Drupal\Core\State\StateInterface
*/
protected $state;
/**
* The count of the number of batches run.
*
* @var int
*/
protected $batch = 0;
/**
* Number of records to fetch from the database during each batch.
*
* A value of zero indicates no batching is to be done.
*
* @var int
*/
protected $batchSize = 0;
/**
* {@inheritdoc}
*/
public function __construct(array $configuration, $plugin_id, $plugin_definition, MigrationInterface $migration, StateInterface $state) {
parent::__construct($configuration, $plugin_id, $plugin_definition, $migration);
$this->state = $state;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition, MigrationInterface $migration = NULL) {
return new static(
$configuration,
$plugin_id,
$plugin_definition,
$migration,
$container->get('state')
);
}
/**
* Prints the query string when the object is used as a string.
*
* @return string
* The query string.
*/
public function __toString() {
return (string) $this->query();
}
/**
* Gets the database connection object.
*
* @return \Drupal\Core\Database\Connection
* The database connection.
*/
public function getDatabase() {
if (!isset($this->database)) {
// See if the database info is in state - if not, fallback to
// configuration.
if (isset($this->configuration['database_state_key'])) {
$this->database = $this->setUpDatabase($this->state->get($this->configuration['database_state_key']));
}
elseif (($fallback_state_key = $this->state->get('migrate.fallback_state_key'))) {
$this->database = $this->setUpDatabase($this->state->get($fallback_state_key));
}
else {
$this->database = $this->setUpDatabase($this->configuration);
}
}
return $this->database;
}
/**
* Gets a connection to the referenced database.
*
* This method will add the database connection if necessary.
*
* @param array $database_info
* Configuration for the source database connection. The keys are:
* 'key' - The database connection key.
* 'target' - The database connection target.
* 'database' - Database configuration array as accepted by
* Database::addConnectionInfo.
*
* @return \Drupal\Core\Database\Connection
* The connection to use for this plugin's queries.
*/
protected function setUpDatabase(array $database_info) {
if (isset($database_info['key'])) {
$key = $database_info['key'];
}
else {
$key = 'migrate';
}
if (isset($database_info['target'])) {
$target = $database_info['target'];
}
else {
$target = 'default';
}
if (isset($database_info['database'])) {
Database::addConnectionInfo($key, $target, $database_info['database']);
}
return Database::getConnection($target, $key);
}
/**
* 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);
}
/**
* Adds 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() {
// Initialize the batch size.
if ($this->batchSize == 0 && isset($this->configuration['batch_size'])) {
// Valid batch sizes are integers >= 0.
if (is_int($this->configuration['batch_size']) && ($this->configuration['batch_size']) >= 0) {
$this->batchSize = $this->configuration['batch_size'];
}
else {
throw new MigrateException("batch_size must be greater than or equal to zero");
}
}
// If a batch has run the query is already setup.
if ($this->batch == 0) {
$this->prepareQuery();
// Get the key values, for potential use in joining to the map table.
$keys = array();
// The rules for determining what conditions to add to the query are as
// follows (applying first applicable rule):
// 1. 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 (empty($this->configuration['ignore_map']) && $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'] . '.' . $this->query->escapeField($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->getDestinationIds())) {
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');
}
// 2. 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 ($this->getHighWaterProperty() && ($high_water = $this->getHighWater()) !== '') {
$high_water_field = $this->getHighWaterField();
$conditions->condition($high_water_field, $high_water, '>');
$this->query->orderBy($high_water_field);
}
if ($condition_added) {
$this->query->condition($conditions);
}
}
// Download data in batches for performance.
if (($this->batchSize > 0)) {
$this->query->range($this->batch * $this->batchSize, $this->batchSize);
}
return new \IteratorIterator($this->query->execute());
}
/**
* Position the iterator to the following row.
*/
protected function fetchNextRow() {
$this->getIterator()->next();
// We might be out of data entirely, or just out of data in the current
// batch. Attempt to fetch the next batch and see.
if ($this->batchSize > 0 && !$this->getIterator()->valid()) {
$this->fetchNextBatch();
}
}
/**
* Prepares query for the next set of data from the source database.
*/
protected function fetchNextBatch() {
$this->batch++;
unset($this->iterator);
$this->getIterator()->rewind();
}
/**
* @return \Drupal\Core\Database\Query\SelectInterface
*/
abstract public function query();
/**
* {@inheritdoc}
*/
public function count() {
return $this->query()->countQuery()->execute()->fetchField();
}
/**
* Checks 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;
}
// With batching, we want a later batch to return the same rows that would
// have been returned at the same point within a monolithic query. If we
// join to the map table, the first batch is writing to the map table and
// thus affecting the results of subsequent batches. To be safe, we avoid
// joining to the map table when batching.
if ($this->batchSize > 0) {
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();
// Special handling for sqlite which deals with files.
if ($id_map_database_options['driver'] === 'sqlite' &&
$source_database_options['driver'] === 'sqlite' &&
$id_map_database_options['database'] != $source_database_options['database']
) {
return FALSE;
}
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,48 @@
<?php
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,339 @@
<?php
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;
}
/**
* Clones the row with an empty set of destination values.
*
* @return static
*/
public function cloneWithoutDestination() {
return (new static($this->getSource(), $this->sourceIds, $this->isStub()))->freezeSource();
}
/**
* 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);
}
/**
* Removes destination property.
*
* @param string $property
* The name of the destination property.
*/
public function removeDestinationProperty($property) {
unset($this->rawDestination[$property]);
NestedArray::unsetValue($this->destination, explode(static::PROPERTY_SEPARATOR, $property));
}
/**
* 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,6 @@
name: 'Migrate entity test'
type: module
description: 'Support module for entity destination test.'
package: Testing
version: VERSION
core: 8.x

View file

@ -0,0 +1,36 @@
<?php
namespace Drupal\migrate_entity_test\Entity;
use Drupal\Core\Entity\ContentEntityBase;
use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\Core\Field\BaseFieldDefinition;
/**
* Defines a content entity type that has a string ID.
*
* @ContentEntityType(
* id = "migrate_string_id_entity_test",
* label = @Translation("String id entity test"),
* base_table = "migrate_entity_test_string_id",
* entity_keys = {
* "id" = "id",
* }
* )
*/
class StringIdEntityTest extends ContentEntityBase {
/**
* {@inheritdoc}
*/
public static function baseFieldDefinitions(EntityTypeInterface $entity_type) {
return [
'id' => BaseFieldDefinition::create('integer')
->setSetting('size', 'big')
->setLabel('ID'),
'version' => BaseFieldDefinition::create('string')
->setLabel('Version'),
];
}
}

View file

@ -0,0 +1,5 @@
name: 'Migrate events test'
type: module
package: Testing
version: VERSION
core: 8.x

View file

@ -0,0 +1,39 @@
<?php
namespace Drupal\migrate_events_test\Plugin\migrate\destination;
use Drupal\migrate\Plugin\MigrationInterface;
use Drupal\migrate\Plugin\migrate\destination\DestinationBase;
use Drupal\migrate\Row;
/**
* @MigrateDestination(
* id = "dummy",
* requirements_met = true
* )
*/
class DummyDestination extends DestinationBase {
/**
* {@inheritdoc}
*/
public function getIds() {
$ids['value']['type'] = 'string';
return $ids;
}
/**
* {@inheritdoc}
*/
public function fields(MigrationInterface $migration = NULL) {
return ['value' => 'Dummy value'];
}
/**
* {@inheritdoc}
*/
public function import(Row $row, array $old_destination_id_values = array()) {
return ['value' => $row->getDestinationProperty('value')];
}
}

View file

@ -0,0 +1,8 @@
name: 'Migration external translated test'
type: module
package: Testing
version: VERSION
core: 8.x
dependencies:
- node
- migrate

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