MigrateSqlIdMapTest.php

Same filename in other branches
  1. 9 core/modules/migrate/tests/src/Unit/MigrateSqlIdMapTest.php
  2. 8.9.x core/modules/migrate/tests/src/Unit/MigrateSqlIdMapTest.php
  3. 10 core/modules/migrate/tests/src/Unit/MigrateSqlIdMapTest.php

Namespace

Drupal\Tests\migrate\Unit

File

core/modules/migrate/tests/src/Unit/MigrateSqlIdMapTest.php

View source
<?php

declare (strict_types=1);
namespace Drupal\Tests\migrate\Unit;

use Drupal\sqlite\Driver\Database\sqlite\Connection;
use Drupal\migrate\Plugin\MigrationInterface;
use Drupal\migrate\MigrateException;
use Drupal\migrate\Plugin\MigrateIdMapInterface;
use Drupal\migrate\Row;

/**
 * Tests the SQL ID map plugin.
 *
 * @group migrate
 */
class MigrateSqlIdMapTest extends MigrateTestCase {
    
    /**
     * The migration configuration, initialized to set the ID and destination IDs.
     *
     * @var array
     */
    protected $migrationConfiguration = [
        'id' => 'sql_idmap_test',
    ];
    
    /**
     * The source IDs.
     *
     * @var array
     */
    protected $sourceIds = [
        'source_id_property' => [
            'type' => 'string',
        ],
    ];
    
    /**
     * The destination IDs.
     *
     * @var array
     */
    protected $destinationIds = [
        'destination_id_property' => [
            'type' => 'string',
        ],
    ];
    
    /**
     * The database connection.
     *
     * @var \Drupal\Core\Database\Connection
     */
    protected $database;
    
    /**
     * {@inheritdoc}
     */
    protected function setUp() : void {
        parent::setUp();
        $this->database = $this->getDatabase([]);
    }
    
    /**
     * Saves a single ID mapping row in the database.
     *
     * @param array $map
     *   The row to save.
     */
    protected function saveMap(array $map) {
        $table = 'migrate_map_sql_idmap_test';
        $schema = $this->database
            ->schema();
        // If the table already exists, add any columns which are in the map array,
        // but don't yet exist in the table. Yay, flexibility!
        if ($schema->tableExists($table)) {
            foreach (array_keys($map) as $field) {
                if (!$schema->fieldExists($table, $field)) {
                    $schema->addField($table, $field, [
                        'type' => 'text',
                    ]);
                }
            }
        }
        else {
            $schema->createTable($table, $this->createSchemaFromRow($map));
        }
        $this->database
            ->insert($table)
            ->fields($map)
            ->execute();
    }
    
    /**
     * Creates a test SQL ID map plugin.
     *
     * @return \Drupal\Tests\migrate\Unit\TestSqlIdMap
     *   A SQL ID map plugin test instance.
     */
    protected function getIdMap() {
        $migration = $this->getMigration();
        $plugin = $this->createMock('Drupal\\migrate\\Plugin\\MigrateSourceInterface');
        $plugin->method('getIds')
            ->willReturn($this->sourceIds);
        $migration->method('getSourcePlugin')
            ->willReturn($plugin);
        $plugin = $this->createMock('Drupal\\migrate\\Plugin\\MigrateDestinationInterface');
        $plugin->method('getIds')
            ->willReturn($this->destinationIds);
        $migration->method('getDestinationPlugin')
            ->willReturn($plugin);
        $event_dispatcher = $this->createMock('Symfony\\Contracts\\EventDispatcher\\EventDispatcherInterface');
        $migration_manager = $this->createMock('Drupal\\migrate\\Plugin\\MigrationPluginManagerInterface');
        $id_map = new TestSqlIdMap($this->database, [], 'sql', [], $migration, $event_dispatcher, $migration_manager);
        $migration->method('getIdMap')
            ->willReturn($id_map);
        return $id_map;
    }
    
    /**
     * Sets defaults for SQL ID map plugin tests.
     *
     * @return array
     *   An associative array with the following keys:
     *   - source_row_status
     *   - rollback_action
     *   - hash
     */
    protected function idMapDefaults() : array {
        $defaults = [
            'source_row_status' => MigrateIdMapInterface::STATUS_IMPORTED,
            'rollback_action' => MigrateIdMapInterface::ROLLBACK_DELETE,
            'hash' => '',
        ];
        // By default, the PDO SQLite driver strongly prefers to return strings
        // from SELECT queries. Even for columns that don't store strings. Even
        // if the connection's STRINGIFY_FETCHES attribute is FALSE. This can cause
        // assertSame() calls to fail, since 0 !== '0'. Casting these values to
        // strings isn't the most elegant workaround, but it allows the assertions
        // to pass properly.
        if ($this->database
            ->driver() == 'sqlite') {
            $defaults['source_row_status'] = (string) $defaults['source_row_status'];
            $defaults['rollback_action'] = (string) $defaults['rollback_action'];
        }
        return $defaults;
    }
    
    /**
     * Tests the ID mapping method.
     *
     * Create two ID mappings and update the second to verify that:
     * - saving new to empty tables work.
     * - saving new to nonempty tables work.
     * - updating work.
     */
    public function testSaveIdMapping() : void {
        $source = [
            'source_id_property' => 'source_value',
        ];
        $row = new Row($source, [
            'source_id_property' => [],
        ]);
        $id_map = $this->getIdMap();
        $id_map->saveIdMapping($row, [
            'destination_id_property' => 2,
        ]);
        $expected_result = [
            [
                'sourceid1' => 'source_value',
                'source_ids_hash' => $this->getIdMap()
                    ->getSourceIdsHash($source),
                'destid1' => 2,
            ] + $this->idMapDefaults(),
        ];
        $this->queryResultTest($this->getIdMapContents(), $expected_result);
        $source = [
            'source_id_property' => 'source_value_1',
        ];
        $row = new Row($source, [
            'source_id_property' => [],
        ]);
        $id_map->saveIdMapping($row, [
            'destination_id_property' => 3,
        ]);
        $expected_result[] = [
            'sourceid1' => 'source_value_1',
            'source_ids_hash' => $this->getIdMap()
                ->getSourceIdsHash($source),
            'destid1' => 3,
        ] + $this->idMapDefaults();
        $this->queryResultTest($this->getIdMapContents(), $expected_result);
        $id_map->saveIdMapping($row, [
            'destination_id_property' => 4,
        ]);
        $expected_result[1]['destid1'] = 4;
        $this->queryResultTest($this->getIdMapContents(), $expected_result);
    }
    
    /**
     * Tests the SQL ID map set message method.
     */
    public function testSetMessage() : void {
        $message = $this->createMock('Drupal\\migrate\\MigrateMessageInterface');
        $id_map = $this->getIdMap();
        $id_map->setMessage($message);
        $this->assertEquals($message, $id_map->message);
    }
    
    /**
     * Tests the clear messages method.
     */
    public function testClearMessages() : void {
        $message = 'Hello world.';
        $expected_results = [
            0,
            1,
            2,
            3,
        ];
        $id_map = $this->getIdMap();
        // Insert 4 message for later delete.
        foreach ($expected_results as $key => $expected_result) {
            $id_map->saveMessage([
                'source_id_property' => $key,
            ], $message);
        }
        // Truncate and check that 4 messages were deleted.
        $this->assertSame($id_map->messageCount(), 4);
        $id_map->clearMessages();
        $count = $id_map->messageCount();
        $this->assertSame($count, 0);
    }
    
    /**
     * Tests the getRowsNeedingUpdate method for rows that need an update.
     */
    public function testGetRowsNeedingUpdate() : void {
        $id_map = $this->getIdMap();
        $row_statuses = [
            MigrateIdMapInterface::STATUS_IMPORTED,
            MigrateIdMapInterface::STATUS_NEEDS_UPDATE,
            MigrateIdMapInterface::STATUS_IGNORED,
            MigrateIdMapInterface::STATUS_FAILED,
        ];
        // Create a mapping row for each STATUS constant.
        foreach ($row_statuses as $status) {
            $source = [
                'source_id_property' => 'source_value_' . $status,
            ];
            $row = new Row($source, [
                'source_id_property' => [],
            ]);
            $destination = [
                'destination_id_property' => 'destination_value_' . $status,
            ];
            $id_map->saveIdMapping($row, $destination, $status);
            $expected_results[] = [
                'sourceid1' => 'source_value_' . $status,
                'source_ids_hash' => $this->getIdMap()
                    ->getSourceIdsHash($source),
                'destid1' => 'destination_value_' . $status,
                'source_row_status' => $status,
                'rollback_action' => MigrateIdMapInterface::ROLLBACK_DELETE,
                'hash' => '',
            ];
            // Assert zero rows need an update.
            if ($status == MigrateIdMapInterface::STATUS_IMPORTED) {
                $rows_needing_update = $id_map->getRowsNeedingUpdate(1);
                $this->assertCount(0, $rows_needing_update);
            }
        }
        // Assert that test values exist.
        $this->queryResultTest($this->getIdMapContents(), $expected_results);
        // Assert a single row needs an update.
        $row_needing_update = $id_map->getRowsNeedingUpdate(1);
        $this->assertCount(1, $row_needing_update);
        // Assert the row matches its original source.
        $source_id = $expected_results[MigrateIdMapInterface::STATUS_NEEDS_UPDATE]['sourceid1'];
        $test_row = $id_map->getRowBySource([
            'source_id_property' => $source_id,
        ]);
        // $row_needing_update is an array of objects returned from the database,
        // but $test_row is an array, so the cast is necessary.
        $this->assertSame($test_row, (array) $row_needing_update[0]);
        // Add additional row that needs an update.
        $source = [
            'source_id_property' => 'source_value_multiple',
        ];
        $row = new Row($source, [
            'source_id_property' => [],
        ]);
        $destination = [
            'destination_id_property' => 'destination_value_multiple',
        ];
        $id_map->saveIdMapping($row, $destination, MigrateIdMapInterface::STATUS_NEEDS_UPDATE);
        // Assert multiple rows need an update.
        $rows_needing_update = $id_map->getRowsNeedingUpdate(2);
        $this->assertCount(2, $rows_needing_update);
    }
    
    /**
     * Tests the SQL ID map message count method by counting and saving messages.
     */
    public function testMessageCount() : void {
        $message = 'Hello world.';
        $expected_results = [
            0,
            1,
            2,
            3,
        ];
        $id_map = $this->getIdMap();
        // Test count message multiple times starting from 0.
        foreach ($expected_results as $key => $expected_result) {
            $count = $id_map->messageCount();
            $this->assertSame($expected_result, $count);
            $id_map->saveMessage([
                'source_id_property' => $key,
            ], $message);
        }
    }
    
    /**
     * Tests the SQL ID map save message method.
     */
    public function testMessageSave() : void {
        $message = 'Hello world.';
        $original_values = [
            1 => [
                'message' => $message,
                'level' => MigrationInterface::MESSAGE_ERROR,
            ],
            2 => [
                'message' => $message,
                'level' => MigrationInterface::MESSAGE_WARNING,
            ],
            3 => [
                'message' => $message,
                'level' => MigrationInterface::MESSAGE_NOTICE,
            ],
            4 => [
                'message' => $message,
                'level' => MigrationInterface::MESSAGE_INFORMATIONAL,
            ],
        ];
        $expected_results = [
            '7ad742edb7e866caa78ced1e4455d2e9cbd8adb2074e7c323d21b4e67732e755' => [
                'message' => $message,
                'level' => MigrationInterface::MESSAGE_ERROR,
            ],
            '2d3ec2b0c547e819346e6ae03f881fd9f5c978ff3cbe29dfb807d40735e53703' => [
                'message' => $message,
                'level' => MigrationInterface::MESSAGE_WARNING,
            ],
            '12a042f72cad9a2a8c7715df0c7695d762975f0687d87f5d480725dae1432a6f' => [
                'message' => $message,
                'level' => MigrationInterface::MESSAGE_NOTICE,
            ],
            'd9d1fd27a2447ace48f47a2e9ff649673f67b446d9381a7963c949fc083f8791' => [
                'message' => $message,
                'level' => MigrationInterface::MESSAGE_INFORMATIONAL,
            ],
        ];
        $id_map = $this->getIdMap();
        foreach ($original_values as $key => $original_value) {
            $id_map->saveMessage([
                'source_id_property' => $key,
            ], $message, $original_value['level']);
        }
        foreach ($id_map->getMessages() as $message_row) {
            $key = $message_row->source_ids_hash;
            $this->assertEquals($expected_results[$key]['message'], $message_row->message);
            $this->assertEquals($expected_results[$key]['level'], $message_row->level);
        }
        // Insert with default level.
        $message_default = 'Hello world default.';
        $id_map->saveMessage([
            'source_id_property' => 5,
        ], $message_default);
        $messages = $id_map->getMessages([
            'source_id_property' => 5,
        ]);
        $count = 0;
        foreach ($messages as $key => $message_row) {
            $count = 1;
            $this->assertEquals($message_default, $message_row->message);
            $this->assertEquals(MigrationInterface::MESSAGE_ERROR, $message_row->level);
        }
        $this->assertEquals(1, $count);
        // Retrieve messages with a specific level.
        $messages = $id_map->getMessages([], MigrationInterface::MESSAGE_WARNING);
        $count = 0;
        foreach ($messages as $key => $message_row) {
            $count = 1;
            $this->assertEquals(MigrationInterface::MESSAGE_WARNING, $message_row->level);
        }
        $this->assertEquals(1, $count);
    }
    
    /**
     * Tests the getRowBySource method.
     */
    public function testGetRowBySource() : void {
        $this->getDatabase([]);
        $row = [
            'sourceid1' => 'source_id_value_1',
            'sourceid2' => 'source_id_value_2',
            'source_ids_hash' => $this->getIdMap()
                ->getSourceIdsHash([
                'source_id_property' => 'source_id_value_1',
            ]),
            'destid1' => 'destination_id_value_1',
        ] + $this->idMapDefaults();
        $this->saveMap($row);
        $row = [
            'sourceid1' => 'source_id_value_3',
            'sourceid2' => 'source_id_value_4',
            'source_ids_hash' => $this->getIdMap()
                ->getSourceIdsHash([
                'source_id_property' => 'source_id_value_3',
                'sourceid2' => 'source_id_value_4',
            ]),
            'destid1' => 'destination_id_value_2',
        ] + $this->idMapDefaults();
        $this->saveMap($row);
        $source_id_values = [
            'source_id_property' => $row['sourceid1'],
            'sourceid2' => $row['sourceid2'],
        ];
        $id_map = $this->getIdMap();
        $result_row = $id_map->getRowBySource($source_id_values);
        $this->assertSame($row, $result_row);
        $source_id_values = [
            'source_id_property' => 'missing_value_1',
            'sourceid2' => 'missing_value_2',
        ];
        $result_row = $id_map->getRowBySource($source_id_values);
        $this->assertFalse($result_row);
    }
    
    /**
     * Data provider for testLookupDestinationIdMapping().
     *
     * Scenarios to test (for both hits and misses) are:
     * - Single-value source ID to single-value destination ID.
     * - Multi-value source ID to multi-value destination ID.
     * - Single-value source ID to multi-value destination ID.
     * - Multi-value source ID to single-value destination ID.
     *
     * @return array
     *   An array of data values.
     */
    public static function lookupDestinationIdMappingDataProvider() {
        return [
            [
                1,
                1,
            ],
            [
                2,
                2,
            ],
            [
                1,
                2,
            ],
            [
                2,
                1,
            ],
        ];
    }
    
    /**
     * Performs destination ID test on source and destination fields.
     *
     * @param int $num_source_fields
     *   Number of source fields to test.
     * @param int $num_destination_fields
     *   Number of destination fields to test.
     *
     * @dataProvider lookupDestinationIdMappingDataProvider
     */
    public function testLookupDestinationIdMapping($num_source_fields, $num_destination_fields) : void {
        // Adjust the migration configuration according to the number of source and
        // destination fields.
        $this->sourceIds = [];
        $this->destinationIds = [];
        $source_id_values = [];
        $nonexistent_id_values = [];
        $row = $this->idMapDefaults();
        for ($i = 1; $i <= $num_source_fields; $i++) {
            $row["sourceid{$i}"] = "source_id_value_{$i}";
            $source_id_values[] = "source_id_value_{$i}";
            $nonexistent_id_values[] = "nonexistent_source_id_value_{$i}";
            $this->sourceIds["source_id_property_{$i}"] = [];
        }
        $expected_result = [];
        for ($i = 1; $i <= $num_destination_fields; $i++) {
            $row["destid{$i}"] = "destination_id_value_{$i}";
            $expected_result[] = "destination_id_value_{$i}";
            $this->destinationIds["destination_id_property_{$i}"] = [];
        }
        $row['source_ids_hash'] = $this->getIdMap()
            ->getSourceIdsHash($source_id_values);
        $this->saveMap($row);
        $id_map = $this->getIdMap();
        // Test for a valid hit.
        $destination_ids = $id_map->lookupDestinationIds($source_id_values);
        $this->assertSame([
            $expected_result,
        ], $destination_ids);
        // Test for a miss.
        $destination_ids = $id_map->lookupDestinationIds($nonexistent_id_values);
        $this->assertCount(0, $destination_ids);
    }
    
    /**
     * Setup a database with the given rows.
     *
     * @param array $source_keys
     *   The source keys for the ID map table.
     * @param array $dest_keys
     *   The destination keys for the ID map table.
     * @param array $rows
     *   An array of source and destination value arrays for the ID map table.
     *
     * @return \Drupal\Tests\migrate\Unit\TestSqlIdMap
     *   An ID map instance for testing.
     */
    protected function setupRows($source_keys, $dest_keys, $rows) {
        $this->database = $this->getDatabase([]);
        $this->sourceIds = array_fill_keys($source_keys, []);
        $this->destinationIds = array_fill_keys($dest_keys, []);
        $db_keys = [];
        foreach (array_keys($source_keys) as $i) {
            $db_keys[] = 'sourceid' . ($i + 1);
        }
        foreach (array_keys($dest_keys) as $i) {
            $db_keys[] = 'destid' . ($i + 1);
        }
        foreach ($rows as $row) {
            $values = array_combine($db_keys, $row);
            $source_values = array_slice($row, 0, count($source_keys));
            $values['source_ids_hash'] = $this->getIdMap()
                ->getSourceIdsHash($source_values);
            $this->saveMap($values);
        }
        return $this->getIdMap();
    }
    
    /**
     * Tests lookupDestinationIds().
     */
    public function testLookupDestinationIds() : void {
        // Simple map with one source and one destination ID.
        $id_map = $this->setupRows([
            'nid',
        ], [
            'nid',
        ], [
            [
                1,
                101,
            ],
            [
                2,
                102,
            ],
            [
                3,
                103,
            ],
        ]);
        // Lookup nothing, gives nothing.
        $this->assertEquals([], $id_map->lookupDestinationIds([]));
        // Lookup by complete non-associative list.
        $this->assertEquals([
            [
                101,
            ],
        ], $id_map->lookupDestinationIds([
            1,
        ]));
        $this->assertEquals([
            [
                102,
            ],
        ], $id_map->lookupDestinationIds([
            2,
        ]));
        $this->assertEquals([], $id_map->lookupDestinationIds([
            99,
        ]));
        // Lookup by complete associative list.
        $this->assertEquals([
            [
                101,
            ],
        ], $id_map->lookupDestinationIds([
            'nid' => 1,
        ]));
        $this->assertEquals([
            [
                102,
            ],
        ], $id_map->lookupDestinationIds([
            'nid' => 2,
        ]));
        $this->assertEquals([], $id_map->lookupDestinationIds([
            'nid' => 99,
        ]));
        // Map with multiple source and destination IDs.
        $id_map = $this->setupRows([
            'nid',
            'language',
        ], [
            'nid',
            'langcode',
        ], [
            [
                1,
                'en',
                101,
                'en',
            ],
            [
                1,
                'fr',
                101,
                'fr',
            ],
            [
                1,
                'de',
                101,
                'de',
            ],
            [
                2,
                'en',
                102,
                'en',
            ],
        ]);
        // Lookup nothing, gives nothing.
        $this->assertEquals([], $id_map->lookupDestinationIds([]));
        // Lookup by complete non-associative list.
        $this->assertEquals([
            [
                101,
                'en',
            ],
        ], $id_map->lookupDestinationIds([
            1,
            'en',
        ]));
        $this->assertEquals([
            [
                101,
                'fr',
            ],
        ], $id_map->lookupDestinationIds([
            1,
            'fr',
        ]));
        $this->assertEquals([
            [
                102,
                'en',
            ],
        ], $id_map->lookupDestinationIds([
            2,
            'en',
        ]));
        $this->assertEquals([], $id_map->lookupDestinationIds([
            2,
            'fr',
        ]));
        $this->assertEquals([], $id_map->lookupDestinationIds([
            99,
            'en',
        ]));
        // Lookup by complete associative list.
        $this->assertEquals([
            [
                101,
                'en',
            ],
        ], $id_map->lookupDestinationIds([
            'nid' => 1,
            'language' => 'en',
        ]));
        $this->assertEquals([
            [
                101,
                'fr',
            ],
        ], $id_map->lookupDestinationIds([
            'nid' => 1,
            'language' => 'fr',
        ]));
        $this->assertEquals([
            [
                102,
                'en',
            ],
        ], $id_map->lookupDestinationIds([
            'nid' => 2,
            'language' => 'en',
        ]));
        $this->assertEquals([], $id_map->lookupDestinationIds([
            'nid' => 2,
            'language' => 'fr',
        ]));
        $this->assertEquals([], $id_map->lookupDestinationIds([
            'nid' => 99,
            'language' => 'en',
        ]));
        // Lookup by partial non-associative list.
        $this->assertEquals([
            [
                101,
                'en',
            ],
            [
                101,
                'fr',
            ],
            [
                101,
                'de',
            ],
        ], $id_map->lookupDestinationIds([
            1,
        ]));
        $this->assertEquals([
            [
                102,
                'en',
            ],
        ], $id_map->lookupDestinationIds([
            2,
        ]));
        $this->assertEquals([], $id_map->lookupDestinationIds([
            99,
        ]));
        // Lookup by partial associative list.
        $this->assertEquals([
            [
                101,
                'en',
            ],
            [
                101,
                'fr',
            ],
            [
                101,
                'de',
            ],
        ], $id_map->lookupDestinationIds([
            'nid' => 1,
        ]));
        $this->assertEquals([
            [
                102,
                'en',
            ],
        ], $id_map->lookupDestinationIds([
            'nid' => 2,
        ]));
        $this->assertEquals([], $id_map->lookupDestinationIds([
            'nid' => 99,
        ]));
        $this->assertEquals([
            [
                101,
                'en',
            ],
            [
                101,
                'fr',
            ],
            [
                101,
                'de',
            ],
        ], $id_map->lookupDestinationIds([
            'nid' => 1,
            'language' => NULL,
        ]));
        $this->assertEquals([
            [
                102,
                'en',
            ],
        ], $id_map->lookupDestinationIds([
            'nid' => 2,
            'language' => NULL,
        ]));
        // Out-of-order partial associative list.
        $this->assertEquals([
            [
                101,
                'en',
            ],
            [
                102,
                'en',
            ],
        ], $id_map->lookupDestinationIds([
            'language' => 'en',
        ]));
        $this->assertEquals([
            [
                101,
                'fr',
            ],
        ], $id_map->lookupDestinationIds([
            'language' => 'fr',
        ]));
        $this->assertEquals([], $id_map->lookupDestinationIds([
            'language' => 'zh',
        ]));
        // Error conditions.
        try {
            $id_map->lookupDestinationIds([
                1,
                2,
                3,
            ]);
            $this->fail('Too many source IDs should throw');
        } catch (MigrateException $e) {
            $this->assertEquals("Extra unknown items for map migrate_map_sql_idmap_test in source IDs: array (\n  0 => 3,\n)", $e->getMessage());
        }
        try {
            $id_map->lookupDestinationIds([
                'nid' => 1,
                'aaa' => '2',
            ]);
            $this->fail('Unknown source ID key should throw');
        } catch (MigrateException $e) {
            $this->assertEquals("Extra unknown items for map migrate_map_sql_idmap_test in source IDs: array (\n  'aaa' => '2',\n)", $e->getMessage());
        }
        // Verify that we are looking up by source_id_hash when all source IDs are
        // passed in.
        $id_map->getDatabase()
            ->update($id_map->mapTableName())
            ->condition('sourceid1', 1)
            ->condition('sourceid2', 'en')
            ->fields([
            TestSqlIdMap::SOURCE_IDS_HASH => uniqid(),
        ])
            ->execute();
        $this->assertNotEquals([
            [
                101,
                'en',
            ],
        ], $id_map->lookupDestinationIds([
            1,
            'en',
        ]));
    }
    
    /**
     * Tests the getRowByDestination method.
     */
    public function testGetRowByDestination() : void {
        $row = [
            'sourceid1' => 'source_id_value_1',
            'sourceid2' => 'source_id_value_2',
            'source_ids_hash' => $this->getIdMap()
                ->getSourceIdsHash([
                'source_id_property' => 'source_id_value_1',
            ]),
            'destid1' => 'destination_id_value_1',
        ] + $this->idMapDefaults();
        $this->saveMap($row);
        $row = [
            'sourceid1' => 'source_id_value_3',
            'sourceid2' => 'source_id_value_4',
            'source_ids_hash' => $this->getIdMap()
                ->getSourceIdsHash([
                'source_id_property' => 'source_id_value_3',
            ]),
            'destid1' => 'destination_id_value_2',
        ] + $this->idMapDefaults();
        $this->saveMap($row);
        $dest_id_values = [
            'destination_id_property' => $row['destid1'],
        ];
        $id_map = $this->getIdMap();
        $result_row = $id_map->getRowByDestination($dest_id_values);
        $this->assertSame($row, $result_row);
        // This value does not exist, getRowByDestination should return an (empty)
        // array.
        // @see \Drupal\migrate\Plugin\MigrateIdMapInterface::getRowByDestination()
        $missing_result_row = $id_map->getRowByDestination([
            'destination_id_property' => 'invalid_destination_id_property',
        ]);
        $this->assertEquals([], $missing_result_row);
        // The destination ID values array does not contain all the destination ID
        // keys, we expect an empty array.
        $invalid_result_row = $id_map->getRowByDestination([
            'invalid_destination_key' => 'invalid_destination_id_property',
        ]);
        $this->assertEquals([], $invalid_result_row);
    }
    
    /**
     * Data provider for testLookupSourceIdMapping().
     *
     * Scenarios to test (for both hits and misses) are:
     * - Single-value destination ID to single-value source ID.
     * - Multi-value destination ID to multi-value source ID.
     * - Single-value destination ID to multi-value source ID.
     * - Multi-value destination ID to single-value source ID.
     *
     * @return array
     *   An array of data values.
     */
    public static function lookupSourceIdMappingDataProvider() {
        return [
            [
                1,
                1,
            ],
            [
                2,
                2,
            ],
            [
                1,
                2,
            ],
            [
                2,
                1,
            ],
        ];
    }
    
    /**
     * Performs the source ID test on source and destination fields.
     *
     * @param int $num_source_fields
     *   Number of source fields to test.
     * @param int $num_destination_fields
     *   Number of destination fields to test.
     *
     * @dataProvider lookupSourceIdMappingDataProvider
     */
    public function testLookupSourceIdMapping($num_source_fields, $num_destination_fields) : void {
        $source_id_property_prefix = 'source_id_property_';
        $this->doTestLookupSourceIdMapping($num_source_fields, $num_destination_fields, $source_id_property_prefix);
    }
    
    /**
     * Performs the source ID test on source and destination fields.
     *
     * This performs same test as ::testLookupSourceIdMapping, except with source
     * property names including spaces and special characters not allowed in SQL
     * column aliases.
     *
     * @param int $num_source_fields
     *   Number of source fields to test.
     * @param int $num_destination_fields
     *   Number of destination fields to test.
     *
     * @dataProvider lookupSourceIdMappingDataProvider
     */
    public function testLookupSourceIdMappingNonSqlCharacters($num_source_fields, $num_destination_fields) : void {
        $source_id_property_prefix = '$ource id property * ';
        $this->doTestLookupSourceIdMapping($num_source_fields, $num_destination_fields, $source_id_property_prefix);
    }
    
    /**
     * Performs the source ID test on source and destination fields.
     *
     * @param int $num_source_fields
     *   Number of source fields to test.
     * @param int $num_destination_fields
     *   Number of destination fields to test.
     * @param string $source_id_property_prefix
     *   Prefix for the source ID properties.
     *
     * @dataProvider lookupSourceIdMappingDataProvider
     */
    public function doTestLookupSourceIdMapping(int $num_source_fields, int $num_destination_fields, string $source_id_property_prefix) : void {
        // Adjust the migration configuration according to the number of source and
        // destination fields.
        $this->sourceIds = [];
        $this->destinationIds = [];
        $row = $this->idMapDefaults();
        $source_ids_values = [];
        $expected_result = [];
        for ($i = 1; $i <= $num_source_fields; $i++) {
            $row["sourceid{$i}"] = "source_id_value_{$i}";
            $source_ids_values = [
                $row["sourceid{$i}"],
            ];
            $expected_result[$source_id_property_prefix . $i] = "source_id_value_{$i}";
            $this->sourceIds[$source_id_property_prefix . $i] = [];
        }
        $destination_id_values = [];
        $nonexistent_id_values = [];
        for ($i = 1; $i <= $num_destination_fields; $i++) {
            $row["destid{$i}"] = "destination_id_value_{$i}";
            $destination_id_values["destination_id_property_{$i}"] = "destination_id_value_{$i}";
            $nonexistent_id_values["destination_id_property_{$i}"] = "nonexistent_destination_id_value_{$i}";
            $this->destinationIds["destination_id_property_{$i}"] = [];
        }
        $row['source_ids_hash'] = $this->getIdMap()
            ->getSourceIdsHash($source_ids_values);
        $this->saveMap($row);
        $id_map = $this->getIdMap();
        // Test for a valid hit.
        $source_id = $id_map->lookupSourceId($destination_id_values);
        $this->assertSame($expected_result, $source_id);
        // Test for a miss.
        $source_id = $id_map->lookupSourceId($nonexistent_id_values);
        $this->assertCount(0, $source_id);
    }
    
    /**
     * Tests currentDestination() and currentSource().
     */
    public function testCurrentDestinationAndSource() : void {
        // Simple map with one source and one destination ID.
        $id_map = $this->setupRows([
            'nid',
        ], [
            'nid',
        ], [
            [
                1,
                101,
            ],
            [
                2,
                102,
            ],
            [
                3,
                103,
            ],
            // Mock a failed row by setting the destination ID to NULL.
[
                4,
                NULL,
            ],
        ]);
        // The rows are ordered by destination ID so the failed row should be first.
        $id_map->rewind();
        $this->assertEquals([], $id_map->currentDestination());
        $this->assertEquals([
            'nid' => 4,
        ], $id_map->currentSource());
        $id_map->next();
        $this->assertEquals([
            'nid' => 101,
        ], $id_map->currentDestination());
        $this->assertEquals([
            'nid' => 1,
        ], $id_map->currentSource());
        $id_map->next();
        $this->assertEquals([
            'nid' => 102,
        ], $id_map->currentDestination());
        $this->assertEquals([
            'nid' => 2,
        ], $id_map->currentSource());
        $id_map->next();
        $this->assertEquals([
            'nid' => 103,
        ], $id_map->currentDestination());
        $this->assertEquals([
            'nid' => 3,
        ], $id_map->currentSource());
        $id_map->next();
    }
    
    /**
     * Tests the imported count method.
     *
     * Scenarios to test for:
     * - No imports.
     * - One import.
     * - Multiple imports.
     */
    public function testImportedCount() : void {
        $id_map = $this->getIdMap();
        // Add a single failed row and assert zero imported rows.
        $source = [
            'source_id_property' => 'source_value_failed',
        ];
        $row = new Row($source, [
            'source_id_property' => [],
        ]);
        $destination = [
            'destination_id_property' => 'destination_value_failed',
        ];
        $id_map->saveIdMapping($row, $destination, MigrateIdMapInterface::STATUS_FAILED);
        $this->assertSame(0, $id_map->importedCount());
        // Add an imported row and assert single count.
        $source = [
            'source_id_property' => 'source_value_imported',
        ];
        $row = new Row($source, [
            'source_id_property' => [],
        ]);
        $destination = [
            'destination_id_property' => 'destination_value_imported',
        ];
        $id_map->saveIdMapping($row, $destination, MigrateIdMapInterface::STATUS_IMPORTED);
        $this->assertSame(1, $id_map->importedCount());
        // Add a row needing update and assert multiple imported rows.
        $source = [
            'source_id_property' => 'source_value_update',
        ];
        $row = new Row($source, [
            'source_id_property' => [],
        ]);
        $destination = [
            'destination_id_property' => 'destination_value_update',
        ];
        $id_map->saveIdMapping($row, $destination, MigrateIdMapInterface::STATUS_NEEDS_UPDATE);
        $this->assertSame(2, $id_map->importedCount());
    }
    
    /**
     * Tests the number of processed source rows.
     *
     * Scenarios to test for:
     * - No processed rows.
     * - One processed row.
     * - Multiple processed rows.
     */
    public function testProcessedCount() : void {
        $id_map = $this->getIdMap();
        // Assert zero rows have been processed before adding rows.
        $this->assertSame(0, $id_map->processedCount());
        $row_statuses = [
            MigrateIdMapInterface::STATUS_IMPORTED,
            MigrateIdMapInterface::STATUS_NEEDS_UPDATE,
            MigrateIdMapInterface::STATUS_IGNORED,
            MigrateIdMapInterface::STATUS_FAILED,
        ];
        // Create a mapping row for each STATUS constant.
        foreach ($row_statuses as $status) {
            $source = [
                'source_id_property' => 'source_value_' . $status,
            ];
            $row = new Row($source, [
                'source_id_property' => [],
            ]);
            $destination = [
                'destination_id_property' => 'destination_value_' . $status,
            ];
            $id_map->saveIdMapping($row, $destination, $status);
            if ($status == MigrateIdMapInterface::STATUS_IMPORTED) {
                // Assert a single row has been processed.
                $this->assertSame(1, $id_map->processedCount());
            }
        }
        // Assert multiple rows have been processed.
        $this->assertSame(count($row_statuses), $id_map->processedCount());
    }
    
    /**
     * Data provider for testUpdateCount().
     *
     * Scenarios to test for:
     * - No updates.
     * - One update.
     * - Multiple updates.
     *
     * @return array
     *   An array of data values.
     */
    public static function updateCountDataProvider() {
        return [
            [
                0,
            ],
            [
                1,
            ],
            [
                3,
            ],
        ];
    }
    
    /**
     * Performs the update count test with a given number of update rows.
     *
     * @param int $num_update_rows
     *   The number of update rows to test.
     *
     * @dataProvider updateCountDataProvider
     */
    public function testUpdateCount($num_update_rows) : void {
        for ($i = 0; $i < 5; $i++) {
            $row = $this->idMapDefaults();
            $row['sourceid1'] = "source_id_value_{$i}";
            $row['source_ids_hash'] = $this->getIdMap()
                ->getSourceIdsHash([
                'source_id_property' => $row['sourceid1'],
            ]);
            $row['destid1'] = "destination_id_value_{$i}";
            $row['source_row_status'] = MigrateIdMapInterface::STATUS_IMPORTED;
            $this->saveMap($row);
        }
        for (; $i < 5 + $num_update_rows; $i++) {
            $row = $this->idMapDefaults();
            $row['sourceid1'] = "source_id_value_{$i}";
            $row['source_ids_hash'] = $this->getIdMap()
                ->getSourceIdsHash([
                'source_id_property' => $row['sourceid1'],
            ]);
            $row['destid1'] = "destination_id_value_{$i}";
            $row['source_row_status'] = MigrateIdMapInterface::STATUS_NEEDS_UPDATE;
            $this->saveMap($row);
        }
        $id_map = $this->getIdMap();
        $this->assertSame($num_update_rows, $id_map->updateCount());
    }
    
    /**
     * Data provider for testErrorCount().
     *
     * Scenarios to test for:
     * - No errors.
     * - One error.
     * - Multiple errors.
     *
     * @return array
     *   An array of data values.
     */
    public static function errorCountDataProvider() {
        return [
            [
                0,
            ],
            [
                1,
            ],
            [
                3,
            ],
        ];
    }
    
    /**
     * Performs error count test with a given number of error rows.
     *
     * @param int $num_error_rows
     *   Number of error rows to test.
     *
     * @dataProvider errorCountDataProvider
     */
    public function testErrorCount($num_error_rows) : void {
        for ($i = 0; $i < 5; $i++) {
            $row = $this->idMapDefaults();
            $row['sourceid1'] = "source_id_value_{$i}";
            $row['source_ids_hash'] = $this->getIdMap()
                ->getSourceIdsHash([
                'source_id_property' => $row['sourceid1'],
            ]);
            $row['destid1'] = "destination_id_value_{$i}";
            $row['source_row_status'] = MigrateIdMapInterface::STATUS_IMPORTED;
            $this->saveMap($row);
        }
        for (; $i < 5 + $num_error_rows; $i++) {
            $row = $this->idMapDefaults();
            $row['sourceid1'] = "source_id_value_{$i}";
            $row['source_ids_hash'] = $this->getIdMap()
                ->getSourceIdsHash([
                'source_id_property' => $row['sourceid1'],
            ]);
            $row['destid1'] = "destination_id_value_{$i}";
            $row['source_row_status'] = MigrateIdMapInterface::STATUS_FAILED;
            $this->saveMap($row);
        }
        $this->assertSame($num_error_rows, $this->getIdMap()
            ->errorCount());
    }
    
    /**
     * Tests setting a row source_row_status to STATUS_NEEDS_UPDATE.
     */
    public function testSetUpdate() : void {
        $id_map = $this->getIdMap();
        $row_statuses = [
            MigrateIdMapInterface::STATUS_IMPORTED,
            MigrateIdMapInterface::STATUS_NEEDS_UPDATE,
            MigrateIdMapInterface::STATUS_IGNORED,
            MigrateIdMapInterface::STATUS_FAILED,
        ];
        // Create a mapping row for each STATUS constant.
        foreach ($row_statuses as $status) {
            $source = [
                'source_id_property' => 'source_value_' . $status,
            ];
            $row = new Row($source, [
                'source_id_property' => [],
            ]);
            $destination = [
                'destination_id_property' => 'destination_value_' . $status,
            ];
            $id_map->saveIdMapping($row, $destination, $status);
            $expected_results[] = [
                'sourceid1' => 'source_value_' . $status,
                'source_ids_hash' => $this->getIdMap()
                    ->getSourceIdsHash($source),
                'destid1' => 'destination_value_' . $status,
                'source_row_status' => $status,
                'rollback_action' => MigrateIdMapInterface::ROLLBACK_DELETE,
                'hash' => '',
            ];
        }
        // Assert that test values exist.
        $this->queryResultTest($this->getIdMapContents(), $expected_results);
        // Mark each row as STATUS_NEEDS_UPDATE.
        foreach ($row_statuses as $status) {
            $id_map->setUpdate([
                'source_id_property' => 'source_value_' . $status,
            ]);
        }
        // Update expected results.
        foreach ($expected_results as $key => $value) {
            $expected_results[$key]['source_row_status'] = MigrateIdMapInterface::STATUS_NEEDS_UPDATE;
        }
        // Assert that updated expected values match.
        $this->queryResultTest($this->getIdMapContents(), $expected_results);
        // Assert an exception is thrown when source identifiers are not provided.
        try {
            $id_map->setUpdate([]);
            $this->assertFalse(FALSE, 'MigrateException not thrown, when source identifiers were provided to update.');
        } catch (MigrateException) {
            $this->assertTrue(TRUE, "MigrateException thrown, when source identifiers were not provided to update.");
        }
    }
    
    /**
     * Tests prepareUpdate().
     */
    public function testPrepareUpdate() : void {
        $id_map = $this->getIdMap();
        $row_statuses = [
            MigrateIdMapInterface::STATUS_IMPORTED,
            MigrateIdMapInterface::STATUS_NEEDS_UPDATE,
            MigrateIdMapInterface::STATUS_IGNORED,
            MigrateIdMapInterface::STATUS_FAILED,
        ];
        // Create a mapping row for each STATUS constant.
        foreach ($row_statuses as $status) {
            $source = [
                'source_id_property' => 'source_value_' . $status,
            ];
            $row = new Row($source, [
                'source_id_property' => [],
            ]);
            $destination = [
                'destination_id_property' => 'destination_value_' . $status,
            ];
            $id_map->saveIdMapping($row, $destination, $status);
            $expected_results[] = [
                'sourceid1' => 'source_value_' . $status,
                'destid1' => 'destination_value_' . $status,
                'source_row_status' => $status,
                'rollback_action' => MigrateIdMapInterface::ROLLBACK_DELETE,
                'hash' => '',
            ];
        }
        // Assert that test values exist.
        $this->queryResultTest($this->getIdMapContents(), $expected_results);
        // Mark all rows as STATUS_NEEDS_UPDATE.
        $id_map->prepareUpdate();
        // Update expected results.
        foreach ($expected_results as $key => $value) {
            $expected_results[$key]['source_row_status'] = MigrateIdMapInterface::STATUS_NEEDS_UPDATE;
        }
        // Assert that updated expected values match.
        $this->queryResultTest($this->getIdMapContents(), $expected_results);
    }
    
    /**
     * Tests the destroy method.
     *
     * Scenarios to test for:
     * - No errors.
     * - One error.
     * - Multiple errors.
     */
    public function testDestroy() : void {
        $id_map = $this->getIdMap();
        // Initialize the ID map.
        $id_map->getDatabase();
        $map_table_name = $id_map->mapTableName();
        $message_table_name = $id_map->messageTableName();
        $row = new Row([
            'source_id_property' => 'source_value',
        ], [
            'source_id_property' => [],
        ]);
        $id_map->saveIdMapping($row, [
            'destination_id_property' => 2,
        ]);
        $id_map->saveMessage([
            'source_id_property' => 'source_value',
        ], 'A message');
        $this->assertTrue($this->database
            ->schema()
            ->tableExists($map_table_name), "{$map_table_name} exists");
        $this->assertTrue($this->database
            ->schema()
            ->tableExists($message_table_name), "{$message_table_name} exists");
        $id_map->destroy();
        $this->assertFalse($this->database
            ->schema()
            ->tableExists($map_table_name), "{$map_table_name} does not exist");
        $this->assertFalse($this->database
            ->schema()
            ->tableExists($message_table_name), "{$message_table_name} does not exist");
    }
    
    /**
     * Tests the getQualifiedMapTable method with a prefixed database.
     */
    public function testGetQualifiedMapTablePrefix() : void {
        $connection_options = [
            'database' => ':memory:',
            'prefix' => 'prefix',
        ];
        $pdo = Connection::open($connection_options);
        $this->database = new Connection($pdo, $connection_options);
        $qualified_map_table = $this->getIdMap()
            ->getQualifiedMapTableName();
        // The SQLite driver is a special flower. It will prefix tables with
        // PREFIX.TABLE, instead of the standard PREFIXTABLE.
        // @see \Drupal\sqlite\Driver\Database\sqlite\Connection::__construct()
        $this->assertEquals('prefix.migrate_map_sql_idmap_test', $qualified_map_table);
    }
    
    /**
     * Tests all the iterator methods in one swing.
     *
     * The iterator methods are:
     * - Sql::rewind()
     * - Sql::next()
     * - Sql::valid()
     * - Sql::key()
     * - Sql::current()
     */
    public function testIterators() : void {
        for ($i = 0; $i < 3; $i++) {
            $row = $this->idMapDefaults();
            $row['sourceid1'] = "source_id_value_{$i}";
            $row['source_ids_hash'] = $this->getIdMap()
                ->getSourceIdsHash([
                'source_id_property' => $row['sourceid1'],
            ]);
            $row['destid1'] = "destination_id_value_{$i}";
            $row['source_row_status'] = MigrateIdMapInterface::STATUS_IMPORTED;
            $expected_results[serialize([
                'sourceid1' => $row['sourceid1'],
            ])] = [
                'destid1' => $row['destid1'],
            ];
            $this->saveMap($row);
        }
        $this->assertSame(iterator_to_array($this->getIdMap()), $expected_results);
    }
    
    /**
     * Retrieves the contents of an ID map.
     *
     * @return array
     *   The contents of an ID map.
     */
    private function getIdMapContents() : array {
        $result = $this->database
            ->select('migrate_map_sql_idmap_test', 't')
            ->fields('t')
            ->execute();
        // The return value needs to be countable, or it will fail certain
        // assertions. iterator_to_array() will not suffice because it won't
        // respect the PDO fetch mode, if specified.
        $contents = [];
        foreach ($result as $row) {
            $contents[] = (array) $row;
        }
        return $contents;
    }
    
    /**
     * Tests the delayed creation of the "map" and "message" migrate tables.
     */
    public function testMapTableCreation() : void {
        $id_map = $this->getIdMap();
        $map_table_name = $id_map->mapTableName();
        $message_table_name = $id_map->messageTableName();
        // Check that tables names do exist.
        $this->assertEquals('migrate_map_sql_idmap_test', $map_table_name);
        $this->assertEquals('migrate_message_sql_idmap_test', $message_table_name);
        // Check that tables don't exist.
        $this->assertFalse($this->database
            ->schema()
            ->tableExists($map_table_name));
        $this->assertFalse($this->database
            ->schema()
            ->tableExists($message_table_name));
        $id_map->getDatabase();
        // Check that tables do exist.
        $this->assertTrue($this->database
            ->schema()
            ->tableExists($map_table_name));
        $this->assertTrue($this->database
            ->schema()
            ->tableExists($message_table_name));
    }
    
    /**
     * Tests getHighestId method.
     *
     * @param array $destination_ids
     *   Array of destination ids.
     * @param array $rows
     *   Array of map table rows.
     * @param int $expected
     *   Expected highest id value.
     *
     * @dataProvider getHighestIdDataProvider
     */
    public function testGetHighestId(array $destination_ids, array $rows, $expected) : void {
        $this->database = $this->getDatabase([]);
        $this->sourceIds = $destination_ids;
        $this->destinationIds = $destination_ids;
        $db_keys = [];
        $dest_id_count = count($destination_ids);
        for ($i = 1; $i <= $dest_id_count; $i++) {
            $db_keys[$i] = "sourceid{$i}";
        }
        for ($i = 1; $i <= $dest_id_count; $i++) {
            $db_keys[] = "destid{$i}";
        }
        $id_map = $this->getIdMap();
        foreach ($rows as $row) {
            $values = array_combine($db_keys, $row);
            $source_values = array_slice($row, 0, $dest_id_count);
            $values['source_ids_hash'] = $id_map->getSourceIdsHash($source_values);
            $this->saveMap($values);
        }
        $actual = $id_map->getHighestId();
        $this->assertSame($expected, $actual);
    }
    
    /**
     * Data provider for getHighestId().
     *
     * Scenarios to test:
     * - Destination ID type integer.
     * - Destination ID types integer and string.
     *
     * @return array
     *   An array of data values.
     */
    public static function getHighestIdDataProvider() {
        return [
            'Destination ID type integer' => [
                'destination_ids' => [
                    'nid' => [
                        'type' => 'integer',
                    ],
                ],
                'rows' => [
                    [
                        1,
                        2,
                    ],
                    [
                        2,
                        1,
                    ],
                    [
                        4,
                        3,
                    ],
                    [
                        9,
                        5,
                    ],
                ],
                'expected' => 5,
            ],
            'Destination ID types integer and string' => [
                'destination_ids' => [
                    'nid' => [
                        'type' => 'integer',
                    ],
                    'vid' => [
                        'type' => 'integer',
                    ],
                    'language' => [
                        'type' => 'string',
                    ],
                ],
                'rows' => [
                    [
                        1,
                        1,
                        'en',
                        1,
                        6,
                        'en',
                    ],
                    [
                        1,
                        4,
                        'fr',
                        1,
                        6,
                        'fr',
                    ],
                    [
                        1,
                        6,
                        'de',
                        1,
                        6,
                        'de',
                    ],
                    [
                        2,
                        8,
                        'en',
                        2,
                        8,
                        'en',
                    ],
                ],
                'expected' => 2,
            ],
        ];
    }
    
    /**
     * Tests getHighestId method with invalid data.
     *
     * @param array $destination_ids
     *   Array of destination ids.
     *
     * @dataProvider getHighestIdInvalidDataProvider
     */
    public function testGetHighestIdInvalid(array $destination_ids) : void {
        $this->expectException(\LogicException::class);
        $this->expectExceptionMessage('To determine the highest migrated ID the first ID must be an integer');
        $this->destinationIds = $destination_ids;
        $id_map = $this->getIdMap();
        $id_map->getHighestId();
    }
    
    /**
     * Data provider for testGetHighestIdInvalid().
     *
     * Scenarios to test:
     * - Destination ID type string.
     * - Destination ID types int (not integer) and string.
     *
     * @return array
     *   An array of data values.
     */
    public static function getHighestIdInvalidDataProvider() {
        return [
            'Destination ID type string' => [
                'destination_ids' => [
                    'language' => [
                        'type' => 'string',
                    ],
                ],
            ],
            'Destination ID types int (not integer) and string' => [
                'destination_ids' => [
                    'nid' => [
                        'type' => 'int',
                    ],
                    'language' => [
                        'type' => 'string',
                    ],
                ],
            ],
        ];
    }

}

Classes

Title Deprecated Summary
MigrateSqlIdMapTest Tests the SQL ID map plugin.

Buggy or inaccurate documentation? Please file an issue. Need support? Need help programming? Connect with the Drupal community.