Problem/Motivation

This is a follow-on to #2443081: Make MigrateExecutable statistics publicly available, particularly https://www.drupal.org/node/2443081#comment-10004395 forward. We would like the Drupal 8 migration ecosystem to be at least as rich as it was in Drupal 7, but right now from contrib we don't have enough insight into what's happening during a migration to do that without additional core support. Rather than build things like counters for items processed/imported/ignored/etc. into the core services, a more flexible approach would be to dispatch events at "interesting" points in the migration process, allowing contrib/custom modules to add feedback and control mechanisms without cluttering up the core services. With these events, a bunch of logically front-end responsibilities (such as time/memory limiting) now in the core service can be moved to contrib (see #2541580: Remove obsoleted functionality from core Migrate, and a number of issues requesting features to help contrib implement front-end features can be closed.

Proposed resolution

  1. Add events to ID map save and delete operations - immediately, this allows us to remove statistics tracking entirely from core and let the contrib tools have complete control. #2443081: Make MigrateExecutable statistics publicly available
  2. Use events to implement the missing pre/post import/rollback functionality. #2501731: Migrate pre/post Import/Rollback methods not invoked
  3. Add events before and after calling the destination import process, allowing per-row operations equivalent to Migrate 2's prepare() and complete() methods.

Remaining tasks

  1. Implement tests for the new events.
  2. Close a bunch of core issues that are no longer needed.

User interface changes

N/A.

API changes

Addition of events.

Data model changes

N/A

Support from Acquia helps fund testing for Drupal Acquia logo

Comments

mikeryan’s picture

Status: Active » Needs work
FileSize
19.46 KB

Here's the last patch from #2443081: Make MigrateExecutable statistics publicly available, implementing the ID map events, rerolled. Getting too late today, but my next step will be adding the pre/post import/rollback (first we have to implement rolling back) events.

mikeryan’s picture

mikeryan’s picture

dawehner’s picture

Add events to ID map save and delete operations - immediately, this allows us to remove statistics tracking entirely from core and let the contrib tools have complete control. #2443081: Make MigrateExecutable statistics publicly available

Nice!

  1. +++ b/core/modules/migrate/src/Plugin/migrate/id_map/Sql.php
    @@ -139,8 +150,8 @@ class Sql extends PluginBase implements MigrateIdMapInterface {
    +    $this->eventDispatcher = $migration->getEventDispatcher();
    

    Conceptually I think this is wrong. THe event dispatcher should be part of the migration executable not of the migration entity itself.

  2. +++ b/core/modules/migrate/tests/src/Unit/MigrateSqlIdMapEnsureTablesTest.php
    @@ -194,6 +194,10 @@ protected function runEnsureTablesTest($schema) {
    +      ->will($this->returnValue($event_dispatcher));
    
    +++ b/core/modules/migrate/tests/src/Unit/MigrateSqlIdMapTest.php
    @@ -102,7 +102,11 @@ protected function getIdMap() {
    +      ->will($this->returnValue($event_dispatcher));
    

    You can use ->willReturnValue() instead

mikeryan’s picture

Conceptually I think this is wrong. The event dispatcher should be part of the migration executable not of the migration entity itself.

Could you elaborate? The events I'm anticipating will be dispatched from migration classes and their plugins, so I don't see the advantage of putting the dispatcher in the executable.

mikeryan’s picture

But, now looking at the executable a little more closely, I can see places events may be useful from there as well...

mikeryan’s picture

@dawehner - Then again, how are you seeing MigrateExecutable's dispatcher being used from, say, the id map plugin? Right now it retrieves it from its migration, do we really want it to know anything about MigrateExecutable?

mikeryan’s picture

FileSize
24.4 KB
6.05 KB

Adding pre/post import events, which addresses (at least) #2501731: Migrate pre/post Import/Rollback methods not invoked and #2429089: Track the last imported time for migrations. See #2536454: Use event listeners to implement a bunch of stuff, where migrate_plus is using the events to report statistics when importing, and last imported time.

mikeryan’s picture

Status: Needs work » Needs review
Issue tags: +Needs tests
Related issues: +#2522012: Remove broken idlist handling, replace with more robust exception handling
FileSize
48.22 KB
25.81 KB

OK, added pre/post save events (dispatched before and after calling the destination import()). This immediately allows contrib migrate_tools to implement --limit and --feedback without requiring any specific support in the core API. And, although I haven't fully integrated it into migrate_tools yet, it allows us to take the time limit and memory limit stuff out of the core MigrateExecutable - with these events, contrib is able to implement that stuff on the frontend, where it belongs. I'm thinking the idlist support (#2522012: Remove broken idlist handling, replace with more robust exception handling ) could also be done in contrib as well with these events, although we'll also want a means to alter the query to optimize SQL-based sources.

At this point, this patch is plenty big enough (and maybe it should even be split out) - I'll wait for feedback before adding any more events.

mikeryan’s picture

19 files changed, 553 insertions(+), 712 deletions(-)

mikeryan’s picture

phenaproxima’s picture

Status: Needs review » Needs work
  1. I agree with @dawehner that the event dispatcher does not belong on the Migration entity. Given the way it's used here, I don't see any reason why it should be. The Sql ID map plugin should take the event dispatcher from the container via ContainerFactoryPluginInterface.
  2. This is not related, but I noticed it -- why does SqlBase have a $migration property? It already inherits one from SourcePluginBase.
  3. In MigrateExecutable, calls to dispatch() should be all on one line for consistency.
  4. Also in MigrateExecutable, why bother with having the event dispatcher in the migration? IMO, getEventDispatcher() should be moved wholesale into MigrateExecutable.
  5. Can all the event types (and MigrateEvents) be in the Drupal\migrate\Event namespace?
  6. MigratePostImportEvent and MigratePreImportEvent are identical. Can they be collapsed into a single class (MigrateImportEvent)?
  7. MigratePreSaveEvent and MigratePostSaveEvent are confusingly named. They're row-level events. Can they be called MigratePreRowSaveEvent and MigratePostRowSaveEvent? They're also mostly identical; MigratePostSaveEvent could extend MigratePreSaveEvent.
  8. In MigrateSqlIdMapTest::getIdMap():
    $migration->expects($this->any())
      ->method('getEventDispatcher')
      ->will($this->returnValue($event_dispatcher));
      

    This can be simplified to $migration->method('getEventDispatcher')->willReturn($event_dispatcher).

  9. Ditto in MigrateTestCase::getMigration().
mikeryan’s picture

Status: Needs work » Needs review
FileSize
53.8 KB
39.21 KB

Here's a new patch reflecting @phenaproxima's suggestions, interdiff is hefty...

Overall: 22 files changed, 492 insertions(+), 781 deletions(-)

mikeryan’s picture

Status: Needs review » Needs work

Capturing (before I forget again), @phenaproxima pointed out we can avoid the getEventDispatcher() method in the id map plugin (added just to support mocking in unit tests) with the "reflection hack":

[4:03pm] phenaproxima: mikeryan1: $prop = (new \ReflectionClass($class))->getProperty('some_inaccessible_property')
[4:03pm] phenaproxima: mikeryan1: $prop->setAccessible(true)
[4:03pm] phenaproxima: mikeryan1: $prop->setValue($object_instance, $value)
mikeryan’s picture

Issue summary: View changes
mikeryan’s picture

Issue summary: View changes
mikeryan’s picture

Status: Needs work » Needs review
Issue tags: -Needs tests
FileSize
64.88 KB
16.87 KB

Tests added - ready for final review.

mikeryan’s picture

With a couple of nits picked by @phenaproxima...

mikeryan’s picture

Issue summary: View changes
benjy’s picture

I had a quick look at the patch and I like it, in general i'm +1 for this. Will try do a more thorough review this week

mikeryan’s picture

Status: Needs review » Needs work

At @EclipseGC's suggestion, splitting this into two patches - addition of the events, and removal of all the stuff that becomes unnecessary with the events.

mikeryan’s picture

Issue summary: View changes
EclipseGc’s picture

  1. +++ b/core/modules/migrate/src/MigrateExecutable.php
    @@ -174,44 +104,32 @@ class MigrateExecutable implements MigrateExecutableInterface {
    +  public function __construct(MigrationInterface $migration, MigrateMessageInterface $message, EventDispatcherInterface $event_dispatcher = NULL) {
    

    Why is the event dispatcher optional in the constructor? Is this being instantiated in such a way that we don't have access to the container outside of this class? Or is this a BC effort of some sort?

  2. +++ b/core/modules/migrate/src/MigrateExecutable.php
    @@ -174,44 +104,32 @@ class MigrateExecutable implements MigrateExecutableInterface {
    +    if (!$event_dispatcher) {
    +      $event_dispatcher = \Drupal::service('event_dispatcher');
    

    Instead of doing this in the constructor, it seems better to do it in a corresponding getter if this needs doing at all. Logic in the constructor should be avoided if possible, and the rest of core does optional calls to \Drupal::service() in getters. Even if it's protected, I think that'll be a "better" approach.

All in all this looks pretty good, but it's hard to mentally divide the two competing patches here. I'd suggest one patch with the event stuff and a follow up to remove the various count/limit/etc stuff in this patch. I think that will make both easier to get in.

Eclipse

mikeryan’s picture

Or is this a BC effort of some sort?

Yes, this is being used in contrib (migrate_plus and migrate_upgrade), so trying not to break users (I'll update those ASAP when this is committed, but there will be people updating core without updating their contrib modules).

mikeryan’s picture

Status: Needs work » Needs review
FileSize
37.54 KB

OK, here it is with just the events added, we'll reap the LOC-- benefits in #2541580: Remove obsoleted functionality from core Migrate.

webchick’s picture

Priority: Normal » Major
Issue tags: +blocker, +Migrate critical

According to mikeryan, this blocks migrate Drush commands (e.g. the ability to do partial imports) from being completed. Escalating.

phenaproxima’s picture

Status: Needs review » Needs work

I have nitpicks, but they're just coding style and terminology-related. Beyond these, the patch looks fine to me.

  1. +++ b/core/modules/migrate/src/Event/MigrateImportEvent.php
    +  /**
    +   * Migration object.
    +   *
    +   * @var \Drupal\migrate\Entity\MigrationInterface
    +   */
    +  protected $migration;
        

    Can this say entity, not object?

  2. +++ b/core/modules/migrate/src/Event/MigrateImportEvent.php
    +  /**
    +   * Constructs an import event object.
    +   *
    +   * @param \Drupal\migrate\Entity\MigrationInterface $migration
    +   *  Migration object.
    +   */
    +  public function __construct(MigrationInterface $migration) {
    +    $this->migration = $migration;
    +  }
        

    $migration's description should be indented another space and be entity, not object.

  3. +++ b/core/modules/migrate/src/Event/MigrateImportEvent.php
    +  /**
    +   * Gets migration object.
    +   *
    +   * @return \Drupal\migrate\Entity\MigrationInterface
    +   *   The migration object involved.
    +   */
    +  public function getMigration() {
    +    return $this->migration;
    +  }
        

    s/object/entity, everywhere it occurs.

  4. +++ b/core/modules/migrate/src/Event/MigrateMapDeleteEvent.php
    +  /**
    +   * Map object.
    +   *
    +   * @var \Drupal\migrate\Plugin\MigrateIdMapInterface
    +   */
    +  protected $map;
        

    s/object/plugin

  5. +  /**
    +   * Constructs a migration map delete event object.
    +   *
    +   * @param \Drupal\migrate\Plugin\MigrateIdMapInterface $map
    +   *  Map object.
    +   * @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;
    +  }
        

    Parameter descriptions should be indented an extra space, and $map's should say 'plugin', not 'object'.

Obviously the indentation and terminology fixes should be applied to all of the event classes, not just the ones listed here. But you get the idea. :)

EclipseGc’s picture

+++ b/core/modules/migrate/src/MigrateExecutable.php
@@ -234,9 +246,23 @@ protected function getSource() {
+  protected function getEventDispatcher() {
+    if (!$this->eventDispatcher) {
+      $this->eventDispatcher = \Drupal::service('event_dispatcher');
+    }
+    return $this->eventDispatcher;
+  }

Sorry if I was unclear before. This looks great, and the NULL eventDispatcher in the __construct() in the previous patch was also great. So constructor injection is still totally ++, but if nothing was injected, this protected getter is also ++. Just don't put this logic in the __construct() method.

Hopefully that makes sense. I've not dug through the tests in depth, but the code changes are looking pretty good here. I'll try to give the tests a once over later today or tomorrow.

mikeryan’s picture

I'll try to give the tests a once over later today or tomorrow.

@phenaproxima suggested a refactor here, so I'd wait until my next patch upload.

mikeryan’s picture

Status: Needs work » Needs review
FileSize
36.71 KB
1.36 KB

Updated for the above review comments. Also, phenaproxima suggested the event listeners could be directly on MigrateEventsTest. I also moved the migration configuration into MigrateEventsTest, at this point most of the migrate_events_test module is gone, but I haven't quite figured out how to move the source/destination plugin classes (because discovery).

$discovery = new StaticDiscovery();
$discovery->setDefinition('data', ['class' => 'Drupal\migrate\Tests\DataSource']);

looked to me like I could get the source plugin registered with ID 'data', but did not work. Thoughts?

mikeryan’s picture

FileSize
18.12 KB

And now the correct interdiff...

benjy’s picture

Patch looks good, nice work! Few small nitpicks and then +1 for RTBC.

  1. +++ b/core/modules/migrate/src/Event/MigrateEvents.php
    @@ -0,0 +1,111 @@
    +   * listener method receives a \Drupal\migrate\MigrateMapDeleteEvent instance.
    

    Should be \Drupal\migrate\Event\MigrateMapDeleteEvent

  2. +++ b/core/modules/migrate/src/Event/MigrateMapSaveEvent.php
    @@ -0,0 +1,65 @@
    +   * Gets map plugin.
    

    Gets the map plugin.

  3. +++ b/core/modules/migrate/src/Event/MigratePreRowSaveEvent.php
    @@ -0,0 +1,64 @@
    +   * Gets migration entity.
    

    Gets the...

  4. +++ b/core/modules/migrate/src/Event/MigratePreRowSaveEvent.php
    @@ -0,0 +1,64 @@
    +   * Gets row object.
    

    Gets the row object.

  5. +++ b/core/modules/migrate/src/MigrateExecutable.php
    @@ -234,9 +249,23 @@ protected function getSource() {
    +   * Returns the event dispatcher.
    

    Gets the event dispatcher

  6. +++ b/core/modules/migrate/tests/modules/migrate_events_test/src/Plugin/migrate/source/DataSource.php
    @@ -0,0 +1,56 @@
    +class DataSource extends SourcePluginBase {
    

    Shame we need this class and could we have re-used EmptySource?

mikeryan’s picture

Docblocks fixed in latest patch.

Shame we need this class and could we have re-used EmptySource?

The problem with EmptySource is, it's empty. We need to actually pass a data row through the pipeline to get all the events triggered with verifiable data.

Thanks!

benjy’s picture

EmptySource does have a row? It's just the id that is empty?

benjy’s picture

Other than that +1 for RTBC

mikeryan’s picture

Ah, it does have a row of sorts, \ArrayIterator(array(array('id' => ''))). But how do we get validatable data migrated out of that?

mikeryan’s picture

phenaproxima’s picture

Status: Needs review » Reviewed & tested by the community

Works for me.

mikeryan’s picture

As requested, clarified use of state to test events.

I'll write up a change record describing the added events.

mikeryan’s picture

webchick’s picture

Status: Reviewed & tested by the community » Fixed

Wow, great CR!!

Went through this patch with @mikeryan and @phenaproxima earlier today. The patch is very well-documented and restores functionality found in Migrate module in D7. Without this, modules such as Migrate Plus can't port.

I inquired about "why events vs. hooks?" and the answer was basically because we're trying to build this in a future-proof way. Since we're writing this in 2015 and not in 2012 and earlier, like most of D8's code, this makes sense to me. I know there have been some concerns about using events in places, but I think it was more when it created a mish-mash of approaches. This is consistent and well-documented.

Committed and pushed to 8.0.x. Thanks!

  • webchick committed a444aaf on 8.0.x
    Issue #2535458 by mikeryan, benjy, phenaproxima, EclipseGc, dawehner:...
webchick’s picture

CR published, too.

Status: Fixed » Closed (fixed)

Automatically closed - issue fixed for 2 weeks with no activity.