Commit a444aafd authored by webchick's avatar webchick

Issue #2535458 by mikeryan, benjy, phenaproxima, EclipseGc, dawehner: Dispatch...

Issue #2535458 by mikeryan, benjy, phenaproxima, EclipseGc, dawehner: Dispatch events at key points during migration
parent 05a8db29
<?php
/**
* @file
* Contains Drupal\migrate\Event\MigrateEvents.
*/
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
*/
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\MigratePreSaveEvent 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';
}
<?php
/**
* @file
* Contains \Drupal\migrate\Event\MigrateImportEvent.
*/
namespace Drupal\migrate\Event;
use Drupal\migrate\Entity\MigrationInterface;
use Symfony\Component\EventDispatcher\Event;
/**
* Wraps a pre- or post-import event for event listeners.
*/
class MigrateImportEvent extends Event {
/**
* Migration entity.
*
* @var \Drupal\migrate\Entity\MigrationInterface
*/
protected $migration;
/**
* Constructs an import event object.
*
* @param \Drupal\migrate\Entity\MigrationInterface $migration
* Migration entity.
*/
public function __construct(MigrationInterface $migration) {
$this->migration = $migration;
}
/**
* Gets the migration entity.
*
* @return \Drupal\migrate\Entity\MigrationInterface
* The migration entity involved.
*/
public function getMigration() {
return $this->migration;
}
}
<?php
/**
* @file
* Contains \Drupal\migrate\Event\MigrateMapDeleteEvent.
*/
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;
}
}
<?php
/**
* @file
* Contains \Drupal\migrate\Event\MigrateMapSaveEvent.
*/
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;
}
}
<?php
/**
* @file
* Contains \Drupal\migrate\Event\MigratePostRowSaveEvent.
*/
namespace Drupal\migrate\Event;
use Drupal\migrate\Entity\MigrationInterface;
use Drupal\migrate\Row;
/**
* Wraps a post-save event for event listeners.
*/
class MigratePostRowSaveEvent extends MigratePreRowSaveEvent {
/**
* Constructs a post-save event object.
*
* @param \Drupal\migrate\Entity\MigrationInterface $migration
* Migration entity.
* @param \Drupal\migrate\Row $row
* Row object.
* @param array|bool $destination_id_values
* Values represent the destination ID.
*/
public function __construct(MigrationInterface $migration, Row $row, $destination_id_values) {
parent::__construct($migration, $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;
}
}
<?php
/**
* @file
* Contains \Drupal\migrate\Event\MigratePreSaveEvent.
*/
namespace Drupal\migrate\Event;
use Drupal\migrate\Entity\MigrationInterface;
use Drupal\migrate\Row;
use Symfony\Component\EventDispatcher\Event;
/**
* Wraps a pre-save event for event listeners.
*/
class MigratePreRowSaveEvent extends Event {
/**
* Row object.
*
* @var \Drupal\migrate\Row
*/
protected $row;
/**
* Migration entity.
*
* @var \Drupal\migrate\Entity\MigrationInterface
*/
protected $migration;
/**
* Constructs a pre-save event object.
*
* @param \Drupal\migrate\Entity\MigrationInterface $migration
* Migration entity.
*/
public function __construct(MigrationInterface $migration, Row $row) {
$this->migration = $migration;
$this->row = $row;
}
/**
* Gets the migration entity.
*
* @return \Drupal\migrate\Entity\MigrationInterface
* The migration entity being imported.
*/
public function getMigration() {
return $this->migration;
}
/**
* Gets the row object.
*
* @return \Drupal\migrate\Row
* The row object about to be imported.
*/
public function getRow() {
return $this->row;
}
}
......@@ -10,8 +10,13 @@
use Drupal\Core\Utility\Error;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Drupal\migrate\Entity\MigrationInterface;
use Drupal\migrate\Event\MigrateEvents;
use Drupal\migrate\Event\MigrateImportEvent;
use Drupal\migrate\Event\MigratePostRowSaveEvent;
use Drupal\migrate\Event\MigratePreRowSaveEvent;
use Drupal\migrate\Exception\RequirementsException;
use Drupal\migrate\Plugin\MigrateIdMapInterface;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
/**
* Defines a migrate executable class.
......@@ -173,6 +178,13 @@ class MigrateExecutable implements MigrateExecutableInterface {
*/
protected $sourceValues;
/**
* The event dispatcher.
*
* @var \Symfony\Component\EventDispatcher\EventDispatcherInterface
*/
protected $eventDispatcher;
/**
* Constructs a MigrateExecutable and verifies and sets the memory limit.
*
......@@ -180,13 +192,16 @@ class MigrateExecutable implements MigrateExecutableInterface {
* 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) {
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') {
......@@ -233,10 +248,24 @@ protected function getSource() {
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() {
$this->getEventDispatcher()->dispatch(MigrateEvents::PRE_IMPORT, new MigrateImportEvent($this->migration));
// Knock off migration if the requirements haven't been met.
try {
$this->migration->checkRequirements();
......@@ -284,7 +313,9 @@ public function import() {
if ($save) {
try {
$this->getEventDispatcher()->dispatch(MigrateEvents::PRE_ROW_SAVE, new MigratePreRowSaveEvent($this->migration, $row));
$destination_id_values = $destination->import($row, $id_map->lookupDestinationId($this->sourceIdValues));
$this->getEventDispatcher()->dispatch(MigrateEvents::POST_ROW_SAVE, new MigratePostRowSaveEvent($this->migration, $row, $destination_id_values));
if ($destination_id_values) {
// We do not save an idMap entry for config.
if ($destination_id_values !== TRUE) {
......@@ -345,6 +376,7 @@ public function import() {
#$this->progressMessage($return);
$this->migration->setMigrationResult($return);
$this->getEventDispatcher()->dispatch(MigrateEvents::POST_IMPORT, new MigrateImportEvent($this->migration));
return $return;
}
......
......@@ -9,13 +9,20 @@
use Drupal\Component\Utility\Unicode;
use Drupal\Core\Database\Connection;
use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\Core\Field\BaseFieldDefinition;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Drupal\Core\Plugin\PluginBase;
use Drupal\migrate\Entity\MigrationInterface;
use Drupal\migrate\MigrateException;
use Drupal\migrate\MigrateMessageInterface;
use Drupal\migrate\Plugin\MigrateIdMapInterface;
use Drupal\migrate\Row;
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.
......@@ -25,7 +32,14 @@
*
* @PluginID("sql")
*/
class Sql extends PluginBase implements MigrateIdMapInterface {
class Sql extends PluginBase implements MigrateIdMapInterface, ContainerFactoryPluginInterface {
/**
* An event dispatcher instance to use for map events.
*
* @var \Symfony\Component\EventDispatcher\EventDispatcherInterface
*/
protected $eventDispatcher;
/**
* The migration map table name.
......@@ -137,10 +151,23 @@ class Sql extends PluginBase implements MigrateIdMapInterface {
* @param \Drupal\migrate\Entity\MigrationInterface $migration
* The migration to do.
*/
public function __construct(array $configuration, $plugin_id, $plugin_definition, MigrationInterface $migration) {
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')
);
}
/**
......@@ -504,6 +531,8 @@ public function saveIdMapping(Row $row, array $destination_id_values, $source_ro
$fields['last_imported'] = time();
}
if ($keys) {
// Notify anyone listening of the map row we're about to save.
$this->eventDispatcher->dispatch(MigrateEvents::MAP_SAVE, new MigrateMapSaveEvent($this, $keys + $fields));
$this->getDatabase()->merge($this->mapTableName())
->key($keys)
->fields($fields)
......@@ -620,6 +649,8 @@ public function delete(array $source_id_values, $messages_only = FALSE) {
}
if (!$messages_only) {
// 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->execute();
......@@ -638,6 +669,8 @@ public function deleteDestination(array $destination_id) {
$map_query->condition('destid' . $count, $key_value);
$count++;
}
// Notify anyone listening of the map row we're about to delete.
$this->eventDispatcher->dispatch(MigrateEvents::MAP_DELETE, new MigrateMapDeleteEvent($this, $source_id));
$map_query->execute();
$count = 1;
foreach ($source_id as $key_value) {
......@@ -673,6 +706,8 @@ public function deleteBulk(array $source_id_values) {
if (count($this->migration->getSourcePlugin()->getIds()) == 1) {
$sourceids = array();
foreach ($source_id_values as $source_id) {
// Notify anyone listening of the map rows we're about to delete.
$this->eventDispatcher->dispatch(MigrateEvents::MAP_DELETE, new MigrateMapDeleteEvent($this, $source_id));
$sourceids[] = $source_id;
}
$this->getDatabase()->delete($this->mapTableName())
......@@ -684,6 +719,8 @@ public function deleteBulk(array $source_id_values) {
}
else {
foreach ($source_id_values as $source_id) {
// Notify anyone listening of the map rows we're deleting.
$this->eventDispatcher->dispatch(MigrateEvents::MAP_DELETE, new MigrateMapDeleteEvent($this, $source_id));
$map_query = $this->getDatabase()->delete($this->mapTableName());
$message_query = $this->getDatabase()->delete($this->messageTableName());
$count = 1;
......
<?php
/**
* @file
* Contains \Drupal\migrate\Tests\MigrateEventsTest.
*/
namespace Drupal\migrate\Tests;
use Drupal\Core\State\State;
use Drupal\migrate\Entity\Migration;
use Drupal\migrate\Event\MigrateImportEvent;
use Drupal\migrate\Event\MigrateMapDeleteEvent;
use Drupal\migrate\Event\MigrateMapSaveEvent;
use Drupal\migrate\Event\MigratePostRowSaveEvent;
use Drupal\migrate\Event\MigratePreRowSaveEvent;
use Drupal\migrate\MigrateMessage;
use Drupal\migrate\Entity\MigrationInterface;
use Drupal\migrate\Event\MigrateEvents;
use Drupal\migrate\MigrateExecutable;
use Drupal\simpletest\KernelTestBase;
/**
* Tests events fired on migrations.
*
* @group migrate
*/
class MigrateEventsTest extends KernelTestBase {
/**
* State service for recording information received by event listeners.
*
* @var \Drupal\Core\State\State
*/
protected $state;
/**
* Modules to enable.
*
* @var array
*/
public static $modules = ['migrate', 'migrate_events_test'];
/**
* {@inheritdoc}
*/
public function setUp() {
parent::setUp();
$this->state = \Drupal::state();
\Drupal::service('event_dispatcher')->addListener(MigrateEvents::MAP_SAVE,
array($this, 'mapSaveEventRecorder'));
\Drupal::service('event_dispatcher')->addListener(MigrateEvents::MAP_DELETE,
array($this, 'mapDeleteEventRecorder'));
\Drupal::service('event_dispatcher')->addListener(MigrateEvents::PRE_IMPORT,
array($this, 'preImportEventRecorder'));
\Drupal::service('event_dispatcher')->addListener(MigrateEvents::POST_IMPORT,
array($this, 'postImportEventRecorder'));
\Drupal::service('event_dispatcher')->addListener(MigrateEvents::PRE_ROW_SAVE,
array($this, 'preRowSaveEventRecorder'));
\Drupal::service('event_dispatcher')->addListener(MigrateEvents::POST_ROW_SAVE,
array($this, 'postRowSaveEventRecorder'));
}
/**
* Tests migration events.
*/
public function testMigrateEvents() {
// Run a simple little migration, which should trigger one of each event
// other than map_delete.
$config = [
'id' => 'sample_data',
'migration_tags' => ['Event test'],
'source' => ['plugin' => 'data'],
'process' => ['value' => 'data'],
'destination' => ['plugin' => 'dummy'],
'load' => ['plugin' => 'null'],
];
$migration = Migration::create($config);
/** @var MigrationInterface $migration */
$executable = new MigrateExecutable($migration, new MigrateMessage);
// As the import runs, events will be dispatched, recording the received
// information in state.
$executable->import();
// Validate from the recorded state that the events were received.
$event = $this->state->get('migrate_events_test.pre_import_event', []);
$this->assertIdentical($event['event_name'], MigrateEvents::PRE_IMPORT);
$this->assertIdentical($event['migration']->id(), $migration->id());
$event = $this->state->get('migrate_events_test.post_import_event', []);
$this->assertIdentical($event['event_name'], MigrateEvents::POST_IMPORT);
$this->assertIdentical($event['migration']->id(), $migration->id());
$event = $this->state->get('migrate_events_test.map_save_event', []);
$this->assertIdentical($event['event_name'], MigrateEvents::MAP_SAVE);
$this->assertIdentical($event['fields']['sourceid1'], 'dummy value');
$this->assertIdentical($event['fields']['destid1'], 'dummy value');
$this->assertIdentical($event['fields']['source_row_status'], 0);
$event = $this->state->get('migrate_events_test.map_delete_event', []);
$this->assertIdentical($event, []);
$event = $this->state->get('migrate_events_test.pre_row_save_event', []);
$this->assertIdentical($event['event_name'], MigrateEvents::PRE_ROW_SAVE);
$this->assertIdentical($event['migration']->id(), $migration->id());
$this->assertIdentical($event['row']->getSourceProperty('data'), 'dummy value');
$event = $this->state->get('migrate_events_test.post_row_save_event', []);
$this->assertIdentical($event['event_name'], MigrateEvents::POST_ROW_SAVE);
$this->assertIdentical($event['migration']->id(), $migration->id());
$this->assertIdentical($event['row']->getSourceProperty('data'), 'dummy value');
$this->assertIdentical($event['destination_id_values']['value'], 'dummy value');
// Generate a map delete event.
$migration->getIdMap()->delete(['data' => 'dummy value']);
$event = $this->state->get('migrate_events_test.map_delete_event', []);
$this->assertIdentical($event['event_name'], MigrateEvents::MAP_DELETE);
$this->assertIdentical($event['source_id'], ['data' => 'dummy value']);
}
/**
* Reacts to map save event.
*
* @param \Drupal\Migrate\Event\MigrateMapSaveEvent $event
* The migration event.
* @param string $name
* The event name.
*/