Verified Commit 8326e01e authored by Dave Long's avatar Dave Long
Browse files

Issue #3520065 by john.oltman, benjifisher: The migrate Row class API is incomplete

parent 04b4e5de
Loading
Loading
Loading
Loading
Loading
+26 −0
Original line number Diff line number Diff line
@@ -266,6 +266,32 @@ public function getEmptyDestinationProperties() {
    return $this->emptyDestinationProperties;
  }

  /**
   * Tests if a property is an empty destination.
   *
   * @param string $property
   *   The name of the property.
   *
   * @return bool
   *   TRUE if the property is an empty destination.
   */
  public function hasEmptyDestinationProperty(string $property): bool {
    return in_array($property, $this->emptyDestinationProperties);
  }

  /**
   * Removes an empty destination property.
   *
   * @param string $property
   *   The name of the empty destination property.
   */
  public function removeEmptyDestinationProperty(string $property): void {
    $this->emptyDestinationProperties = array_diff(
      $this->emptyDestinationProperties,
      [$property],
    );
  }

  /**
   * Returns the whole destination array.
   *
+152 −0
Original line number Diff line number Diff line
<?php

declare(strict_types=1);

namespace Drupal\Tests\migrate\Kernel;

use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\KernelTests\KernelTestBase;
use Drupal\migrate\Event\MigratePreRowSaveEvent;
use Drupal\migrate\Event\MigrateEvents;
use Drupal\migrate\MigrateExecutable;
use Drupal\migrate\Plugin\MigrationPluginManagerInterface;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;

/**
 * Tests the Row class.
 *
 * @group migrate
 */
class RowTest extends KernelTestBase {

  /**
   * The event dispatcher.
   */
  protected EventDispatcherInterface $eventDispatcher;

  /**
   * The entity type manager.
   */
  protected EntityTypeManagerInterface $entityTypeManager;

  /**
   * The migration manager.
   */
  protected MigrationPluginManagerInterface $migrationManager;

  /**
   * {@inheritdoc}
   */
  protected static $modules = [
    'entity_test',
    'field',
    'migrate',
    'user',
  ];

  /**
   * {@inheritdoc}
   */
  protected function setUp(): void {
    parent::setUp();
    $this->installEntitySchema('entity_test');

    $this->eventDispatcher = \Drupal::service('event_dispatcher');
    $this->entityTypeManager = \Drupal::service('entity_type.manager');
    $this->migrationManager = \Drupal::service('plugin.manager.migration');

    // Create two fields that will be set during migration.
    $fields = ['field1', 'field2'];
    foreach ($fields as $field) {
      $this->entityTypeManager->getStorage('field_storage_config')->create([
        'entity_type' => 'entity_test',
        'field_name' => $field,
        'type' => 'string',
      ])->save();
      $this->entityTypeManager->getStorage('field_config')->create([
        'entity_type' => 'entity_test',
        'field_name' => $field,
        'bundle' => 'entity_test',
      ])->save();
    }
  }

  /**
   * Tests the destination properties of the Row class.
   */
  public function testRowDestinations(): void {
    $storage = $this->entityTypeManager->getStorage('entity_test');

    // Execute a migration that creates an entity with two fields.
    $data_rows = [
      ['id' => 1, 'field1' => 'f1value', 'field2' => 'f2value'],
    ];
    $ids = ['id' => ['type' => 'integer']];
    $definition = [
      'source' => [
        'plugin' => 'embedded_data',
        'data_rows' => $data_rows,
        'ids' => $ids,
      ],
      'process' => [
        'id' => 'id',
        'field1' => 'field1',
        'field2' => 'field2',
      ],
      'destination' => ['plugin' => 'entity:entity_test'],
    ];
    $this->executeMigrationImport($definition);
    $entity = $storage->load(1);
    $this->assertEquals('f1value', $entity->get('field1')->getValue()[0]['value']);
    $this->assertEquals('f2value', $entity->get('field2')->getValue()[0]['value']);

    // Execute a second migration that attempts to remove both field values.
    // The event listener prevents the removal of the second field.
    $data_rows = [
      ['id' => 1, 'field1' => NULL, 'field2' => NULL],
    ];
    $definition['source']['data_rows'] = $data_rows;
    $this->eventDispatcher->addListener(MigrateEvents::PRE_ROW_SAVE, [$this, 'preventFieldRemoval']);
    $this->executeMigrationImport($definition);

    // The first field is now empty but the second field is still set.
    $entity = $storage->load(1);
    $this->assertTrue($entity->get('field1')->isEmpty());
    $this->assertEquals('f2value', $entity->get('field2')->getValue()[0]['value']);
  }

  /**
   * The pre-row-save event handler for the second migration.
   *
   * Checks row destinations and prevents the removal of the second field.
   *
   * @param \Drupal\migrate\Event\MigratePreRowSaveEvent $event
   *   The migration event.
   * @param string $name
   *   The event name.
   */
  public function preventFieldRemoval(MigratePreRowSaveEvent $event, string $name): void {
    $row = $event->getRow();

    // Both fields are empty and their existing values will be removed.
    $this->assertFalse($row->hasDestinationProperty('field1'));
    $this->assertFalse($row->hasDestinationProperty('field2'));
    $this->assertTrue($row->hasEmptyDestinationProperty('field1'));
    $this->assertTrue($row->hasEmptyDestinationProperty('field2'));

    // Prevent removal of field 2.
    $row->removeEmptyDestinationProperty('field2');
  }

  /**
   * Executes a migration import for the given migration definition.
   *
   * @param array $definition
   *   The migration definition.
   */
  protected function executeMigrationImport(array $definition): void {
    $migration = $this->migrationManager->createStubMigration($definition);
    (new MigrateExecutable($migration))->import();
  }

}
+37 −0
Original line number Diff line number Diff line
@@ -291,6 +291,43 @@ public function testDestination(): void {
    $this->assertEquals(['nid' => 2], $row->getDestination());
  }

  /**
   * Tests checking for and removing destination properties that may be empty.
   *
   * @covers ::hasEmptyDestinationProperty
   * @covers ::removeEmptyDestinationProperty
   */
  public function testDestinationOrEmptyProperty(): void {
    $row = new Row($this->testValues, $this->testSourceIds);

    // Set a destination.
    $row->setDestinationProperty('nid', 2);
    $this->assertTrue($row->hasDestinationProperty('nid'));
    $this->assertFalse($row->hasEmptyDestinationProperty('nid'));

    // Set an empty destination.
    $row->setEmptyDestinationProperty('a_property_with_no_value');
    $this->assertTrue($row->hasEmptyDestinationProperty('a_property_with_no_value'));
    $this->assertFalse($row->hasDestinationProperty('a_property_with_no_value'));

    // Removing an empty destination that is not actually empty has no effect.
    $row->removeEmptyDestinationProperty('nid');
    $this->assertTrue($row->hasDestinationProperty('nid'));
    $this->assertFalse($row->hasEmptyDestinationProperty('nid'));

    // Removing a destination that is actually empty has no effect.
    $row->removeDestinationProperty('a_property_with_no_value');
    $this->assertTrue($row->hasEmptyDestinationProperty('a_property_with_no_value'));

    // Remove the empty destination.
    $row->removeEmptyDestinationProperty('a_property_with_no_value');
    $this->assertFalse($row->hasEmptyDestinationProperty('a_property_with_no_value'));

    // Removing a destination that does not exist does not throw an error.
    $this->assertFalse($row->hasEmptyDestinationProperty('not_a_property'));
    $row->removeEmptyDestinationProperty('not_a_property');
  }

  /**
   * Tests setting/getting multiple destination IDs.
   */