diff -u b/core/modules/migrate/lib/Drupal/migrate/Entity/Migration.php b/core/modules/migrate/lib/Drupal/migrate/Entity/Migration.php --- b/core/modules/migrate/lib/Drupal/migrate/Entity/Migration.php +++ b/core/modules/migrate/lib/Drupal/migrate/Entity/Migration.php @@ -206,8 +206,8 @@ } $process_plugins = array(); foreach ($this->getProcessNormalized($process) as $property => $configurations) { + $process_plugins[$property] = array(); foreach ($configurations as $configuration) { - $process_plugins[$property] = array(); if (isset($configuration['source'])) { $process_plugins[$property][] = \Drupal::service('plugin.manager.migrate.process')->createInstance('get', $configuration, $this); } @@ -226,10 +226,13 @@ /** * Resolve shorthands into a list of plugin configurations. * + * @param array $process + * A process configuration array. + * * @return array * The normalized process configuration. */ - protected function getProcessNormalized($process) { + protected function getProcessNormalized(array $process) { $normalized_configurations = array(); foreach ($process as $destination => $configuration) { if (is_string($configuration)) { diff -u b/core/modules/migrate/lib/Drupal/migrate/Entity/MigrationInterface.php b/core/modules/migrate/lib/Drupal/migrate/Entity/MigrationInterface.php --- b/core/modules/migrate/lib/Drupal/migrate/Entity/MigrationInterface.php +++ b/core/modules/migrate/lib/Drupal/migrate/Entity/MigrationInterface.php @@ -62,6 +62,7 @@ * * @param array $process * A process configuration array. + * * @return array * A list of process plugins. */ diff -u b/core/modules/migrate/lib/Drupal/migrate/MigrateExecutable.php b/core/modules/migrate/lib/Drupal/migrate/MigrateExecutable.php --- b/core/modules/migrate/lib/Drupal/migrate/MigrateExecutable.php +++ b/core/modules/migrate/lib/Drupal/migrate/MigrateExecutable.php @@ -300,15 +300,27 @@ } /** - * Apply transformations to a data row received from the source. + * @param Row $row + * The $row to be processed. + * @param array $process + * A process pipeline configuration. If not set, the top level process + * configuration in the migration entity is used. + * @param mixed $value + * Optional initial value of the pipeline for the first destination. + * Usually setting this is not necessary as $process typically starts with + * a 'get'. This is useful only when the $process contains a single + * destination and needs to access a value outside of the source. See + * \Drupal\migrate\Plugin\migrate\process\Iterator::transformKey for an + * example. */ - public function processRow(Row $row, array $process = NULL) { - $value = NULL; + public function processRow(Row $row, array $process = NULL, $value = NULL) { foreach ($this->migration->getProcessPlugins($process) as $destination => $plugins) { foreach ($plugins as $plugin) { $value = $plugin->transform($value, $this, $row, $destination); } $row->setDestinationProperty($destination, $value); + // Reset the value. + $value = NULL; } } diff -u b/core/modules/migrate/lib/Drupal/migrate/Plugin/MigrateIdMapInterface.php b/core/modules/migrate/lib/Drupal/migrate/Plugin/MigrateIdMapInterface.php --- b/core/modules/migrate/lib/Drupal/migrate/Plugin/MigrateIdMapInterface.php +++ b/core/modules/migrate/lib/Drupal/migrate/Plugin/MigrateIdMapInterface.php @@ -2,7 +2,7 @@ /** * @file - * Contains \Drupal\migrate\Plugin\IdMapInterface. + * Contains \Drupal\migrate\Plugin\MigrateIdMapInterface. */ namespace Drupal\migrate\Plugin; @@ -13,10 +13,10 @@ use Drupal\migrate\Row; /** - * An interface for migrate ID mappings. + * Defines an interface for migrate ID mappings. * - * Migrate ID mappings maintain a relation between source ID and - * destination ID for audit and rollback purposes. + * Migrate ID mappings maintain a relation between source ID and destination ID + * for audit and rollback purposes. */ interface MigrateIdMapInterface extends PluginInspectionInterface { @@ -32,138 +32,210 @@ * Codes reflecting how to handle the destination item on rollback. - * */ const ROLLBACK_DELETE = 0; const ROLLBACK_PRESERVE = 1; - /** - * Save a mapping from the source identifiers to the destination - * identifiers. - * - * @param $row - * The current row.. - * @param $destination_id_values + /** + * Saves a mapping from the source identifiers to the destination identifiers. + * + * Called upon import of one row, we record a mapping from the source ID + * to the destination ID. Also may be called, setting the third parameter to + * NEEDS_UPDATE, to signal an existing record should be re-migrated. + * + * @param \Drupal\migrate\Row $row + * The raw source data. We use the ID map derived from the source object + * to get the source identifier values. + * @param array $destination_id_values * An array of destination identifier values. - * @param $status - * @param $rollback_action + * @param int $status + * Status of the source row in the map. + * @param int $rollback_action + * How to handle the destination object on rollback. */ public function saveIdMapping(Row $row, array $destination_id_values, $status = self::STATUS_IMPORTED, $rollback_action = self::ROLLBACK_DELETE); /** - * Record a message related to a source record + * Saves a message related to a source record in the migration message table. * * @param array $source_id_values - * Source ID of the record in error + * The source identifier values of the record in error. * @param string $message - * The message to record. + * The message to record. * @param int $level - * Optional message severity (defaults to MESSAGE_ERROR). + * Optional message severity (defaults to MESSAGE_ERROR). */ - public function saveMessage(array $source_id_value, $message, $level = MigrationInterface::MESSAGE_ERROR); + public function saveMessage(array $source_id_values, $message, $level = MigrationInterface::MESSAGE_ERROR); /** - * Prepare to run a full update - mark all previously-imported content as - * ready to be re-imported. + * Prepares to run a full update. + * + * Prepares this migration to run as an update - that is, in addition to + * unmigrated content (source records not in the map table) being imported, + * previously-migrated content will also be updated in place by marking all + * previously-imported content as ready to be re-imported. */ public function prepareUpdate(); /** - * Report the number of processed items in the map + * Returns the number of processed items in the map. + * + * @return int + * The count of records in the map table. */ public function processedCount(); /** - * Report the number of imported items in the map + * Returns the number of imported items in the map. + * + * @return int + * The number of imported items. */ public function importedCount(); + /** - * Report the number of items that failed to import + * Returns a count of items which are marked as needing update. + * + * @return int + * The number of items which need updating. + */ + public function updateCount(); + + /** + * Returns the number of items that failed to import. + * + * @return int + * The number of items that errored out. */ public function errorCount(); /** - * Report the number of messages + * Returns the number of messages saved. + * + * @return int + * The number of messages. */ public function messageCount(); /** - * Delete the map and message entries for a given source record + * Deletes the map and message entries for a given source record. * - * @param array $source_key + * @param array $source_id_values + * The source identifier values of the record to delete. * @param bool $messages_only + * TRUE to only delete the migrate messages. */ - public function delete(array $source_key, $messages_only = FALSE); + public function delete(array $source_id_values, $messages_only = FALSE); /** - * Delete the map and message entries for a given destination record + * Deletes the map and message table entries for a given destination row. * - * @param array $destination_key + * @param array $destination_id_values + * The destination identifier values we should do the deletes for. */ - public function deleteDestination(array $destination_key); + public function deleteDestination(array $destination_id_values); /** - * Delete the map and message entries for a set of given source records. + * Deletes the map and message entries for a set of given source records. * - * @param array $source_ids + * @param array $source_id_values + * The identifier values of the sources we should do the deletes for. Each + * array member is an array of identifier values for one source row. */ - public function deleteBulk(array $source_ids); + public function deleteBulk(array $source_id_values); /** - * Clear all messages from the map. + * Clears all messages from the map. */ public function clearMessages(); /** - * Retrieve map data for a given source or destination item + * Retrieves a row from the map table based on source identifier values. + * + * @param array $source_id_values + * The source identifier values of the record to retrieve. + * + * @return array + * The raw row data as an associative array. */ - public function getRowBySource(array $source_id); - + public function getRowBySource(array $source_id_values); /** - * Retrieve a row by the destination identifiers. + * Retrieves a row by the destination identifiers. * - * @param array $destination_id + * @param array $destination_id_values + * The destination identifier values of the record to retrieve. * * @return array + * The row(s) of data. */ public function getRowByDestination(array $destination_id_values); /** - * Retrieve an array of map rows marked as needing update. + * Retrieves an array of map rows marked as needing update. + * + * @param int $count + * The maximum number of rows to return. + * + * @return array + * Array of map row objects that need updating. */ public function getRowsNeedingUpdate($count); /** - * Given a (possibly multi-field) destination key, return the (possibly multi-field) - * source key mapped to it. + * Looks up the source identifier. + * + * Given a (possibly multi-field) destination identifier value, return the + * (possibly multi-field) source identifier value mapped to it. + * + * @param array $destination_id_values + * The destination identifier values of the record. * - * @param array $destination_id - * Array of destination key values. * @return array - * Array of source key values, or NULL on failure. + * The source identifier values of the record, or NULL on failure. */ - public function lookupSourceID(array $destination_id); + public function lookupSourceID(array $destination_id_values); /** - * Given a (possibly multi-field) source key, return the (possibly multi-field) - * destination key it is mapped to. + * Looks up the destination identifier. + * + * Given a (possibly multi-field) source identifier value, return the + * (possibly multi-field) destination identifier value it is mapped to. + * + * @param array $destination_id_values + * The source identifier values of the record. * - * @param array $source_id_values - * Array of source identifier values. * @return array - * Array of destination key values, or NULL on failure. + * The destination identifier values of the record, or NULL on failure. */ public function lookupDestinationID(array $source_id_values); /** - * Remove any persistent storage used by this map (e.g., map and message tables) + * Removes any persistent storage used by this map. + * + * For example, remove the map and message tables. */ public function destroy(); /** - * @TODO: YUCK THIS IS SQL BOUND! + * Gets the qualified map table. + * + * @todo Remove this as this is SQL only and so doesn't belong to the interface. */ public function getQualifiedMapTable(); + /** + * Sets the migrate message. + * + * @param \Drupal\migrate\MigrateMessageInterface $message + * The message to display. + */ public function setMessage(MigrateMessageInterface $message); + + /** + * Sets a specified record to be updated, if it exists. + * + * @param $source_id_values + * The source identifier values of the record. + */ + public function setUpdate(array $source_id_values); } diff -u b/core/modules/migrate/lib/Drupal/migrate/Plugin/MigrateProcessInterface.php b/core/modules/migrate/lib/Drupal/migrate/Plugin/MigrateProcessInterface.php --- b/core/modules/migrate/lib/Drupal/migrate/Plugin/MigrateProcessInterface.php +++ b/core/modules/migrate/lib/Drupal/migrate/Plugin/MigrateProcessInterface.php @@ -21,14 +21,14 @@ * * @param $value * The value to be transformed. - * @param MigrateExecutable + * @param \Drupal\migrate\MigrateExecutable $migrate_executable * The migration in which this process is being executed. - * @param Row $row + * @param \Drupal\migrate\Row $row * The row from the source to process. Normally, just transforming the * value is adequate but very rarely you might need to change two columns * at the same time or something like that. * @param string $destination_property - * The destionation property currently worked on. This is only used + * The destination property currently worked on. This is only used * together with the $row above. */ public function transform($value, MigrateExecutable $migrate_executable, Row $row, $destination_property); diff -u b/core/modules/migrate/lib/Drupal/migrate/Plugin/RequirementsInterface.php b/core/modules/migrate/lib/Drupal/migrate/Plugin/RequirementsInterface.php --- b/core/modules/migrate/lib/Drupal/migrate/Plugin/RequirementsInterface.php +++ b/core/modules/migrate/lib/Drupal/migrate/Plugin/RequirementsInterface.php @@ -13,7 +13,7 @@ interface RequirementsInterface { /** - * Checks if requiremens for this plugin are OK. + * Checks if requirements for this plugin are OK. * * @return boolean * TRUE if it is possible to use the plugin, FALSE if not. diff -u b/core/modules/migrate/lib/Drupal/migrate/Plugin/migrate/id_map/Sql.php b/core/modules/migrate/lib/Drupal/migrate/Plugin/migrate/id_map/Sql.php --- b/core/modules/migrate/lib/Drupal/migrate/Plugin/migrate/id_map/Sql.php +++ b/core/modules/migrate/lib/Drupal/migrate/Plugin/migrate/id_map/Sql.php @@ -7,10 +7,10 @@ namespace Drupal\migrate\Plugin\migrate\id_map; -use Drupal\Component\Utility\String; use Drupal\Component\Utility\Unicode; use Drupal\Core\Plugin\PluginBase; use Drupal\migrate\Entity\MigrationInterface; +use Drupal\migrate\MigrateException; use Drupal\migrate\MigrateMessageInterface; use Drupal\migrate\Plugin\migrate\source\SqlBase; use Drupal\migrate\Plugin\MigrateIdMapInterface; @@ -34,17 +34,12 @@ protected $mapTable, $messageTable; /** + * The migrate message. + * * @var \Drupal\migrate\MigrateMessageInterface */ protected $message; - public function getMapTable() { - return $this->mapTable; - } - public function getMessageTable() { - return $this->messageTable; - } - /** * The database connection for the map/message tables on the destination. * @@ -58,42 +53,27 @@ protected $query; /** + * The migration being done. + * * @var \Drupal\migrate\Entity\MigrationInterface */ protected $migration; /** + * The source ID fields. + * * @var array */ protected $sourceIdFields; /** + * The destination ID fields. + * * @var array */ protected $destinationIdFields; /** - * Qualifying the map table name with the database name makes cross-db joins - * possible. Note that, because prefixes are applied after we do this (i.e., - * it will prefix the string we return), we do not qualify the table if it has - * a prefix. This will work fine when the source data is in the default - * (prefixed) database (in particular, for simpletest), but not if the primary - * query is in an external database. - * - * @return string - */ - public function getQualifiedMapTable() { - $options = $this->getDatabase()->getConnectionOptions(); - $prefix = $this->getDatabase()->tablePrefix($this->mapTable); - if ($prefix) { - return $this->mapTable; - } - else { - return $options['database'] . '.' . $this->mapTable; - } - } - - /** * Stores whether the the tables (map/message) already exist. * * This is determined just once per request/instance of the class. @@ -102,7 +82,42 @@ */ protected $ensured; - public function __construct($configuration, $plugin_id, $plugin_definition, MigrationInterface $migration) { + /** + * The result. + * + * @var null + */ + protected $result = NULL; + + /** + * The current row. + * + * @var null + */ + protected $currentRow = NULL; + + /** + * The current key. + * + * @var array + */ + protected $currentKey = array(); + + /** + * Constructs an SQL object. + * + * Sets up the tables and builds the maps, + * + * @param array $configuration + * The configuration. + * @param string $plugin_id + * The plugin ID for the migration process to do. + * @param array $plugin_definition + * The configuration for the plugin. + * @param \Drupal\migrate\Entity\MigrationInterface $migration + * The migration to do. + */ + public function __construct(array $configuration, $plugin_id, array $plugin_definition, MigrationInterface $migration) { parent::__construct($configuration, $plugin_id, $plugin_definition); $this->migration = $migration; @@ -117,7 +132,7 @@ $this->sourceIds = $migration->get('sourceIds'); $this->destinationIds = $migration->get('destinationIds'); - // Build the source and destination key maps. + // Build the source and destination identifier maps. $this->sourceIdFields = array(); $count = 1; foreach ($this->sourceIds as $field => $schema) { @@ -131,8 +146,37 @@ $this->ensureTables(); } + /** + * Qualifying the map table name with the database name makes cross-db joins + * possible. Note that, because prefixes are applied after we do this (i.e., + * it will prefix the string we return), we do not qualify the table if it has + * a prefix. This will work fine when the source data is in the default + * (prefixed) database (in particular, for simpletest), but not if the primary + * query is in an external database. + * + * @return string + */ + public function getQualifiedMapTable() { + $options = $this->getDatabase()->getConnectionOptions(); + $prefix = $this->getDatabase()->tablePrefix($this->mapTable); + if ($prefix) { + return $this->mapTable; + } + else { + return $options['database'] . '.' . $this->mapTable; + } + } + + /** + * Gets the database connection. + * + * @return \Drupal\Core\Database\Connection + */ protected function getDatabase() { - return SqlBase::getDatabaseConnection($this->migration->id(), $this->configuration); + if (!isset($this->database)) { + $this->database = SqlBase::getDatabaseConnection($this->migration->id(), $this->configuration); + } + return $this->database; } /** @@ -149,7 +193,7 @@ if (!$this->ensured) { if (!$this->getDatabase()->schema()->tableExists($this->mapTable)) { // Generate appropriate schema info for the map and message tables, - // and map from the source field names to the map/msg field names + // and map from the source field names to the map/msg field names. $count = 1; $source_id_schema = array(); $pks = array(); @@ -161,11 +205,12 @@ $fields = $source_id_schema; - // Add destination keys to map table + // Add destination identifiers to map table. // TODO: How do we discover the destination schema? $count = 1; foreach ($this->destinationIds as $field_schema) { - // Allow dest key fields to be NULL (for IGNORED/FAILED cases) + // Allow dest identifier fields to be NULL (for IGNORED/FAILED + // cases). $field_schema['not null'] = FALSE; $mapkey = 'destid' . $count++; $fields[$mapkey] = $field_schema; @@ -200,7 +245,7 @@ 'description' => 'Hash of source row data, for detecting changes', ); $schema = array( - 'description' => t('Mappings from source key to destination key'), + 'description' => 'Mappings from source identifier value(s) to destination identifier value(s).', 'fields' => $fields, ); if ($pks) { @@ -208,7 +253,7 @@ } $this->getDatabase()->schema()->createTable($this->mapTable, $schema); - // Now for the message table + // Now do the message table. $fields = array(); $fields['msgid'] = array( 'type' => 'serial', @@ -229,7 +274,7 @@ 'not null' => TRUE, ); $schema = array( - 'description' => t('Messages generated during a migration process'), + 'description' => 'Messages generated during a migration process', 'fields' => $fields, 'primary key' => array('msgid'), ); @@ -239,7 +284,7 @@ $this->getDatabase()->schema()->createTable($this->messageTable, $schema); } else { - // Add any missing columns to the map table + // Add any missing columns to the map table. if (!$this->getDatabase()->schema()->fieldExists($this->mapTable, 'rollback_action')) { $this->getDatabase()->schema()->addField($this->mapTable, @@ -266,50 +311,33 @@ } /** - * Retrieve a row from the map table, given a source ID. - * - * @param array $source_id - * A list of source IDs, even there is just one source ID. - * - * @return array - * The raw row data as associative array. + * {@inheritdoc} */ - public function getRowBySource(array $source_id) { + public function getRowBySource(array $source_id_value) { $query = $this->getDatabase()->select($this->mapTable, 'map') ->fields('map'); - foreach ($this->sourceIdFields as $key_name) { - $query = $query->condition("map.$key_name", array_shift($source_id), '='); + foreach ($this->sourceIdFields as $source_id) { + $query = $query->condition("map.$source_id", array_shift($source_id_value), '='); } $result = $query->execute(); return $result->fetchAssoc(); } /** - * Retrieve a row from the map table, given a destination ID - * - * @param array $destination_id - * A list of destination IDs, even there is just one destination ID. - * - * @return mixed - * The row(s) of data + * {@inheritdoc} */ - public function getRowByDestination(array $destination_id) { + public function getRowByDestination(array $destination_id_values) { $query = $this->getDatabase()->select($this->mapTable, 'map') ->fields('map'); - foreach ($this->destinationIdFields as $key_name) { - $query = $query->condition("map.$key_name", array_shift($destination_id), '='); + foreach ($this->destinationIdFields as $destination_id) { + $query = $query->condition("map.$destination_id", array_shift($destination_id_values), '='); } $result = $query->execute(); return $result->fetchAssoc(); } /** - * Retrieve an array of map rows marked as needing update. - * - * @param int $count - * Maximum rows to return; defaults to 10,000 - * @return array - * Array of map row objects with needs_update==1. + * {@inheritdoc} */ public function getRowsNeedingUpdate($count) { $rows = array(); @@ -325,13 +353,7 @@ } /** - * Given a (possibly multi-field) destination key, return the (possibly multi-field) - * source key mapped to it. - * - * @param array $destination_id - * Array of destination key values. - * @return array - * Array of source key values, or NULL on failure. + * {@inheritdoc} */ public function lookupSourceID(array $destination_id) { $query = $this->getDatabase()->select($this->mapTable, 'map') @@ -345,13 +367,7 @@ } /** - * Given a (possibly multi-field) source key, return the (possibly multi-field) - * destination key it is mapped to. - * - * @param array $source_id - * Array of source key values. - * @return array - * Array of destination key values, or NULL on failure. + * {@inheritdoc} */ public function lookupDestinationID(array $source_id) { $query = $this->getDatabase()->select($this->mapTable, 'map') @@ -365,36 +381,22 @@ } /** - * Called upon import of one record, we record a mapping from the source key - * to the destination key. Also may be called, setting the third parameter to - * NEEDS_UPDATE, to signal an existing record should be remigrated. - * - * @param Row $row - * The raw source data. We use the key map derived from the source object - * to get the source key values. - * @param array $dest_ids - * The destination key values. - * @param int $needs_update - * Status of the source row in the map. Defaults to STATUS_IMPORTED. - * @param int $rollback_action - * How to handle the destination object on rollback. Defaults to - * ROLLBACK_DELETE. - * $param string $hash - * If hashing is enabled, the hash of the raw source row. + * {@inheritdoc} */ public function saveIdMapping(Row $row, array $destination_id_values, $needs_update = MigrateIdMapInterface::STATUS_IMPORTED, $rollback_action = MigrateIdMapInterface::ROLLBACK_DELETE) { - // Construct the source key + // Construct the source key. + $source_id_values = $row->getSourceIdValues(); + // Construct the source key and initialize to empty variable keys. $keys = array(); - $destination = $row->getDestination(); foreach ($this->sourceIdFields as $field_name => $key_name) { // A NULL key value will fail. - if (!isset($destination[$field_name])) { + if (!isset($source_id_values[$field_name])) { $this->message->display(t( 'Could not save to map table due to NULL value for key field !field', array('!field' => $field_name))); return; } - $keys[$key_name] = $destination[$field_name]; + $keys[$key_name] = $source_id_values[$field_name]; } $fields = array( @@ -418,21 +420,13 @@ } /** - * Record a message in the migration's message table. - * - * @param array $source_id_values - * Source ID of the record in error - * @param string $message - * The message to record. - * @param int $level - * Optional message severity (defaults to MESSAGE_ERROR). + * {@inheritdoc} */ public function saveMessage(array $source_id_values, $message, $level = MigrationInterface::MESSAGE_ERROR) { - // Source IDs as arguments $count = 1; foreach ($source_id_values as $id_value) { $fields['sourceid' . $count++] = $id_value; - // If any key value is empty, we can't save - print out and abort + // If any key value is empty, we can't save - print out and abort. if (empty($id_value)) { print($message); return; @@ -446,9 +440,7 @@ } /** - * Prepares this migration to run as an update - that is, in addition to - * unmigrated content (source records not in the map table) being imported, - * previously-migrated content will also be updated in place. + * {@inheritdoc} */ public function prepareUpdate() { $this->getDatabase()->update($this->mapTable) @@ -457,87 +449,82 @@ } /** - * Returns a count of records in the map table (i.e., the number of - * source records which have been processed for this migration). - * - * @return int + * {@inheritdoc} */ public function processedCount() { - $query = $this->getDatabase()->select($this->mapTable); - $query->addExpression('COUNT(*)', 'count'); - $count = $query->execute()->fetchField(); - return $count; + return $this->getDatabase()->select($this->mapTable) + ->countQuery() + ->execute() + ->fetchField(); } /** - * Returns a count of imported records in the map table. - * - * @return int + * {@inheritdoc} */ public function importedCount() { - $query = $this->getDatabase()->select($this->mapTable); - $query->addExpression('COUNT(*)', 'count'); - $query->condition('needs_update', array(MigrateIdMapInterface::STATUS_IMPORTED, MigrateIdMapInterface::STATUS_NEEDS_UPDATE), 'IN'); - $count = $query->execute()->fetchField(); - return $count; + return $this->getDatabase()->select($this->mapTable) + ->condition('needs_update', array(MigrateIdMapInterface::STATUS_IMPORTED, MigrateIdMapInterface::STATUS_NEEDS_UPDATE), 'IN') + ->countQuery() + ->execute() + ->fetchField(); } /** - * Returns a count of records which are marked as needing update. - * - * @return int + * {@inheritdoc} */ public function updateCount() { - $query = $this->getDatabase()->select($this->mapTable); - $query->addExpression('COUNT(*)', 'count'); - $query->condition('needs_update', MigrateIdMapInterface::STATUS_NEEDS_UPDATE); - $count = $query->execute()->fetchField(); - return $count; + return $this->countHelper(MigrateIdMapInterface::STATUS_NEEDS_UPDATE); } /** - * Get the number of source records which failed to import. - * - * @return int - * Number of records errored out. + * {@inheritdoc} */ public function errorCount() { - $query = $this->getDatabase()->select($this->mapTable); - $query->addExpression('COUNT(*)', 'count'); - $query->condition('needs_update', MigrateIdMapInterface::STATUS_FAILED); - $count = $query->execute()->fetchField(); - return $count; + return $this->countHelper(MigrateIdMapInterface::STATUS_FAILED); } /** - * Get the number of messages saved. - * - * @return int - * Number of messages. + * {@inheritdoc} */ public function messageCount() { - $query = $this->getDatabase()->select($this->messageTable); - $query->addExpression('COUNT(*)', 'count'); - $count = $query->execute()->fetchField(); - return $count; + return $this->countHelper(NULL, $this->messageTable); } /** - * Delete the map entry and any message table entries for the specified source row. + * Counts records in a table. * - * @param array $source_id + * @param $status + * An integer for the needs_update column. + * @param $table + * The table to work + * @return int + * The number of records. */ - public function delete(array $source_id, $messages_only = FALSE) { + protected function countHelper($status, $table = NULL) { + $query = $this->getDatabase()->select($table ?: $this->mapTable); + if (isset($status)) { + $query->condition('needs_update', $status); + } + return $query->countQuery()->execute()->fetchField(); + } + + /** + * {@inheritdoc} + */ + public function delete(array $source_id_values, $messages_only = FALSE) { + if (empty($source_id_values)) { + throw new MigrateException('Without source identifier values it is impossible to find the row to delete.'); + } if (!$messages_only) { $map_query = $this->getDatabase()->delete($this->mapTable); } $message_query = $this->getDatabase()->delete($this->messageTable); $count = 1; - foreach ($source_id as $key_value) { + foreach ($source_id_values as $id_value) { if (!$messages_only) { - $map_query->condition('sourceid' . $count, $key_value); + $map_query->condition('sourceid' . $count, $id_value); } - $message_query->condition('sourceid' . $count, $key_value); + $message_query->condition('sourceid' . $count, $id_value); $count++; } @@ -548,9 +535,7 @@ } /** - * Delete the map entry and any message table entries for the specified destination row. - * - * @param array $destination_id + * {@inheritdoc} */ public function deleteDestination(array $destination_id) { $map_query = $this->getDatabase()->delete($this->mapTable); @@ -573,11 +558,15 @@ } /** - * Set the specified row to be updated, if it exists. + * {@inheritdoc} */ public function setUpdate(array $source_id) { - $query = $this->getDatabase()->update($this->mapTable) - ->fields(array('needs_update' => MigrateIdMapInterface::STATUS_NEEDS_UPDATE)); + if (empty($source_ids)) { + throw new MigrateException('No source identifiers provided to update.'); + } + $query = $this->getDatabase() + ->update($this->mapTable) + ->fields(array('needs_update' => MigrateIdMapInterface::STATUS_NEEDS_UPDATE)); $count = 1; foreach ($source_id as $key_value) { $query->condition('sourceid' . $count++, $key_value); @@ -586,16 +575,13 @@ } /** - * Delete all map and message table entries specified. - * - * @param array $source_ids - * Each array member is an array of key fields for one source row. + * {@inheritdoc} */ - public function deleteBulk(array $source_ids) { - // If we have a single-column key, we can shortcut it + public function deleteBulk(array $source_id_values) { + // If we have a single-column key, we can shortcut it. if (count($this->sourceIds) == 1) { $sourceids = array(); - foreach ($source_ids as $source_id) { + foreach ($source_id_values as $source_id) { $sourceids[] = $source_id; } $this->getDatabase()->delete($this->mapTable) @@ -606,7 +592,7 @@ ->execute(); } else { - foreach ($source_ids as $source_id) { + foreach ($source_id_values as $source_id) { $map_query = $this->getDatabase()->delete($this->mapTable); $message_query = $this->getDatabase()->delete($this->messageTable); $count = 1; @@ -621,31 +607,26 @@ } /** - * Clear all messages from the message table. + * {@inheritdoc} */ public function clearMessages() { - $this->getDatabase()->truncate($this->messageTable) - ->execute(); + $this->getDatabase()->truncate($this->messageTable)->execute(); } /** - * Remove the associated map and message tables. + * {@inheritdoc} */ public function destroy() { $this->getDatabase()->schema()->dropTable($this->mapTable); $this->getDatabase()->schema()->dropTable($this->messageTable); } - protected $result = NULL; - protected $currentRow = NULL; - protected $currentKey = array(); - public function getCurrentKey() { - return $this->currentKey; - } - /** - * Implementation of Iterator::rewind() - called before beginning a foreach loop. - * TODO: Support idlist, itemlimit + * Implementation of Iterator::rewind(). + * + * This is called before beginning a foreach loop. + * + * @todo Support idlist, itemlimit. */ public function rewind() { $this->currentRow = NULL; @@ -657,7 +638,8 @@ $fields[] = $field; } - /* TODO + // @todo Make this work. + /* if (isset($this->options['itemlimit'])) { $query = $query->range(0, $this->options['itemlimit']); } @@ -669,25 +651,30 @@ } /** - * Implementation of Iterator::current() - called when entering a loop - * iteration, returning the current row + * Implementation of Iterator::current(). + * + * This is called when entering a loop iteration, returning the current row. */ public function current() { return $this->currentRow; } /** - * Implementation of Iterator::key - called when entering a loop iteration, returning - * the key of the current row. It must be a scalar - we will serialize - * to fulfill the requirement, but using getCurrentKey() is preferable. + * Implementation of Iterator::key(). + * + * This is called when entering a loop iteration, returning the key of the + * current row. It must be a scalar - we will serialize to fulfill the + * requirement, but using getCurrentKey() is preferable. */ public function key() { return serialize($this->currentKey); } /** - * Implementation of Iterator::next() - called at the bottom of the loop implicitly, - * as well as explicitly from rewind(). + * Implementation of Iterator::next(). + * + * This is called at the bottom of the loop implicitly, as well as explicitly + * from rewind(). */ public function next() { $this->currentRow = $this->result->fetchObject(); @@ -700,16 +687,19 @@ $this->currentKey[$map_field] = $this->currentRow->$map_field; - // Leave only destination fields + // Leave only destination fields. unset($this->currentRow->$map_field); } } } /** - * Implementation of Iterator::valid() - called at the top of the loop, returning - * TRUE to process the loop and FALSE to terminate it + * Implementation of Iterator::valid(). + * + * This is called at the top of the loop, returning TRUE to process the loop + * and FALSE to terminate it. */ public function valid() { - // TODO: Check numProcessed against itemlimit + // @todo Check numProcessed against itemlimit. return !is_null($this->currentRow); } + } diff -u b/core/modules/migrate/lib/Drupal/migrate/Plugin/migrate/process/DefaultValue.php b/core/modules/migrate/lib/Drupal/migrate/Plugin/migrate/process/DefaultValue.php --- b/core/modules/migrate/lib/Drupal/migrate/Plugin/migrate/process/DefaultValue.php +++ b/core/modules/migrate/lib/Drupal/migrate/Plugin/migrate/process/DefaultValue.php @@ -5,8 +5,8 @@ * Contains \Drupal\migrate\Plugin\migrate\process\DefaultValue. */ - namespace Drupal\migrate\Plugin\migrate\process; + use Drupal\Core\Plugin\PluginBase; use Drupal\migrate\MigrateExecutable; use Drupal\migrate\Plugin\MigrateProcessInterface; @@ -25,5 +25,5 @@ */ public function transform($value, MigrateExecutable $migrate_executable, Row $row, $destination_property) { - return $this->configuration['value']; + return isset($value) ? $value : $this->configuration['default_value']; } } diff -u b/core/modules/migrate/lib/Drupal/migrate/Plugin/migrate/source/SqlBase.php b/core/modules/migrate/lib/Drupal/migrate/Plugin/migrate/source/SqlBase.php --- b/core/modules/migrate/lib/Drupal/migrate/Plugin/migrate/source/SqlBase.php +++ b/core/modules/migrate/lib/Drupal/migrate/Plugin/migrate/source/SqlBase.php @@ -8,11 +8,8 @@ namespace Drupal\migrate\Plugin\migrate\source; use Drupal\Core\Database\Database; -use Drupal\Core\Plugin\PluginBase; use Drupal\migrate\Entity\MigrationInterface; use Drupal\migrate\Plugin\MigrateIdMapInterface; -use Drupal\migrate\Plugin\MigrateSourceInterface; -use Drupal\migrate\Row; /** * Sources whose data may be fetched via DBTNG. diff -u b/core/modules/migrate/lib/Drupal/migrate/Row.php b/core/modules/migrate/lib/Drupal/migrate/Row.php --- b/core/modules/migrate/lib/Drupal/migrate/Row.php +++ b/core/modules/migrate/lib/Drupal/migrate/Row.php @@ -2,7 +2,7 @@ /** * @file - * Contains \Drupal\migrate\MigrateRow. + * Contains \Drupal\migrate\Row. */ namespace Drupal\migrate; @@ -11,7 +11,7 @@ use Drupal\migrate\Plugin\MigrateIdMapInterface; /** - * This just stores a row. + * Stores a row. */ class Row { @@ -64,9 +64,6 @@ * @param array $source_ids * An array containing the IDs of the source using the keys as the field * names. - * @param array $destination_ids - * An array containing the IDs of the destination using the keys as the field - * names. * * @throws \InvalidArgumentException * Thrown when a source ID property does not exist. @@ -120,7 +117,7 @@ } /** - * This returns the whole source array. + * Returns the whole source array. * * @return array * An array of source plugins. @@ -130,7 +127,9 @@ } /** - * Sets a source property. This can only be called from the source plugin. + * Sets a source property. + * + * This can only be called from the source plugin. * * @param string $property * A property on the source. @@ -156,8 +155,13 @@ } /** - * @param array $property_keys + * Tests if destination property exists. + * + * @param array|string $property * An array of properties on the destination. + * + * @return boolean + * TRUE if the destination property exists. */ public function hasDestinationProperty($property) { return NestedArray::keyExists($this->destination, explode(':', $property)); @@ -176,7 +180,7 @@ } /** - * This returns the whole destination array. + * Returns the whole destination array. * * @return array * An array of destination values. @@ -186,7 +190,10 @@ } /** - * Return a the value of a destination property. + * Returns the value of a destination property. + * + * @param array|string $property + * An array of properties on the destination. * * @return mixed * The destination value. @@ -216,7 +223,7 @@ } /** - * Recalculate the hash for the row. + * Recalculates the hash for the row. */ public function rehash() { $this->idMap['original_hash'] = $this->idMap['hash']; @@ -226,7 +233,7 @@ /** * Checks whether the row has changed compared to the original ID map. * - * return bool + * @return bool * TRUE if the row has changed, FALSE otherwise. If setIdMap() was not * called, this always returns FALSE. */ @@ -255,2 +262,3 @@ } + } diff -u b/core/modules/migrate/lib/Drupal/migrate/Source.php b/core/modules/migrate/lib/Drupal/migrate/Source.php --- b/core/modules/migrate/lib/Drupal/migrate/Source.php +++ b/core/modules/migrate/lib/Drupal/migrate/Source.php @@ -184,6 +184,7 @@ * Class constructor. * * @param \Drupal\migrate\Entity\MigrationInterface $migration + * @param \Drupal\migrate\MigrateExecutable $migrate_executable */ function __construct(MigrationInterface $migration, MigrateExecutable $migrate_executable) { $this->migration = $migration; diff -u b/core/modules/migrate/lib/Drupal/migrate/Tests/Dump/Drupal6SystemCron.php b/core/modules/migrate/lib/Drupal/migrate/Tests/Dump/Drupal6SystemCron.php --- b/core/modules/migrate/lib/Drupal/migrate/Tests/Dump/Drupal6SystemCron.php +++ b/core/modules/migrate/lib/Drupal/migrate/Tests/Dump/Drupal6SystemCron.php @@ -15,10 +15,10 @@ class Drupal6SystemCron { /** - * Mock the database schema and values. + * Sample database schema and values. * * @param \Drupal\Core\Database\Connection $database - * The mocked database connection. + * The database connection. */ public static function load(Connection $database) { $database->schema()->createTable('variable', array( diff -u b/core/modules/migrate/lib/Drupal/migrate/Tests/MigrateSystemConfigsTest.php b/core/modules/migrate/lib/Drupal/migrate/Tests/MigrateSystemConfigsTest.php --- b/core/modules/migrate/lib/Drupal/migrate/Tests/MigrateSystemConfigsTest.php +++ b/core/modules/migrate/lib/Drupal/migrate/Tests/MigrateSystemConfigsTest.php @@ -43,7 +43,7 @@ } /** - * Tests migration of book variables to system.cron.yml. + * Tests migration of system (cron) variables to system.cron.yml. */ public function testSystemCron() { $migration = entity_load('migration', 'd6_system_cron'); @@ -59,2 +59,17 @@ } + + /** + * Tests migration of system (rss) variables to system.rss.yml. + */ + public function testSystemRss() { + $migration = entity_load('migration', 'd6_system_rss'); + $dumps = array( + drupal_get_path('module', 'migrate') . '/lib/Drupal/migrate/Tests/Dump/Drupal6SystemRss.php', + ); + $this->prepare($migration, $dumps); + $executable = new MigrateExecutable($migration, new MigrateMessage()); + $executable->import(); + $config = \Drupal::config('system.rss'); + $this->assertIdentical($config->get('items.limit'), 10); + } } diff -u b/core/modules/migrate/lib/Drupal/migrate/Tests/MigrateTestBase.php b/core/modules/migrate/lib/Drupal/migrate/Tests/MigrateTestBase.php --- b/core/modules/migrate/lib/Drupal/migrate/Tests/MigrateTestBase.php +++ b/core/modules/migrate/lib/Drupal/migrate/Tests/MigrateTestBase.php @@ -51,9 +51,8 @@ $file = "compress.zlib://$file"; require $file; } - $namespaces = preg_filter('/^namespace (.*);/', '\1', file($file)); - // trim() is necessary to remove the line break file() keeps. - $class = trim(reset($namespaces)) . '\\' . basename($file, '.php'); + preg_match('/^namespace (.*);$/m', file_get_contents($file), $matches); + $class = $matches[1] . '\\' . basename($file, '.php'); $class::load($database); } return $database; diff -u b/core/modules/migrate/tests/Drupal/migrate/Tests/D6VariableTest.php b/core/modules/migrate/tests/Drupal/migrate/Tests/D6VariableTest.php --- b/core/modules/migrate/tests/Drupal/migrate/Tests/D6VariableTest.php +++ b/core/modules/migrate/tests/Drupal/migrate/Tests/D6VariableTest.php @@ -31,7 +31,7 @@ protected $mapJoinable = FALSE; - protected $results = array( + protected $expectedResults = array( array( 'foo' => 1, 'bar' => FALSE, @@ -50,7 +50,7 @@ */ public static function getInfo() { return array( - 'name' => 'D6 variabe source functionality', + 'name' => 'D6 variable source functionality', 'description' => 'Tests D6 variable source plugin.', 'group' => 'Migrate', ); diff -u b/core/modules/migrate/tests/Drupal/migrate/Tests/FakeDatabaseSchema.php b/core/modules/migrate/tests/Drupal/migrate/Tests/FakeDatabaseSchema.php --- b/core/modules/migrate/tests/Drupal/migrate/Tests/FakeDatabaseSchema.php +++ b/core/modules/migrate/tests/Drupal/migrate/Tests/FakeDatabaseSchema.php @@ -10,12 +10,6 @@ use Drupal\Core\Database\Schema; -use Drupal\Core\Database\Connection; -use Drupal\Core\Database\Query\Condition; -use Drupal\Core\Database\Query\PlaceholderInterface; -use Drupal\Core\Database\Query\Select; -use Drupal\Core\Database\Query\SelectInterface; - class FakeDatabaseSchema extends Schema { /** @@ -23,8 +17,8 @@ */ protected $databaseContents; - public function __construct($connection, $database_contents) { - parent::__construct($connection); + public function __construct($database_contents) { + $this->uniqueIdentifier = uniqid('', TRUE); // @todo Maybe we can generate an internal representation. $this->databaseContents = $database_contents; } @@ -74,7 +68,7 @@ } public function createTable($name, $table) { - throw new \Exception(sprintf('Unsupported method "%s"', __METHOD__)); + #throw new \Exception(sprintf('Unsupported method "%s"', __METHOD__)); } public function dropField($table, $field) { diff -u b/core/modules/migrate/tests/Drupal/migrate/Tests/FakeSelect.php b/core/modules/migrate/tests/Drupal/migrate/Tests/FakeSelect.php --- b/core/modules/migrate/tests/Drupal/migrate/Tests/FakeSelect.php +++ b/core/modules/migrate/tests/Drupal/migrate/Tests/FakeSelect.php @@ -8,7 +8,6 @@ namespace Drupal\migrate\Tests; use Drupal\Core\Database\Connection; -use Drupal\Core\Database\DatabaseNotFoundException; use Drupal\Core\Database\Query\Condition; use Drupal\Core\Database\Query\PlaceholderInterface; use Drupal\Core\Database\Query\Select; @@ -120,20 +119,37 @@ */ public function execute() { // @todo: Implement distinct() handling. - $all_rows = $this->executeJoins(); - $this->resolveConditions($this->where, $all_rows); - if (!empty($this->order)) { - usort($all_rows, array($this, 'sortCallback')); + + // Single table count queries often do not contain fields which this class + // does not support otherwise, so add a shortcut. + if (count($this->tables) == 1 && $this->countQuery) { + $table_info = reset($this->tables); + $where = $this->where; + if (!empty($this->databaseContents[$table_info['table']])) { + $results = array_filter($this->databaseContents[$table_info['table']], function ($row_array) use ($where) { + return ConditionResolver::matchGroup(new DatabaseRow($row_array), $where); + }); + } + else { + $results = array(); + } } - // Now flatten the rows so that each row becomes a field alias => value - // array. - $results = array(); - foreach ($all_rows as $table_rows) { - $result_row = array(); - foreach ($table_rows as $row) { - $result_row += $row['result']; + else { + $all_rows = $this->executeJoins(); + $all_rows = $this->resolveConditions($this->where, $all_rows); + if (!empty($this->order)) { + usort($all_rows, array($this, 'sortCallback')); + } + // Now flatten the rows so that each row becomes a field alias => value + // array. + $results = array(); + foreach ($all_rows as $table_rows) { + $result_row = array(); + foreach ($table_rows as $row) { + $result_row += $row['result']; + } + $results[] = $result_row; } - $results[] = $result_row; } if (!empty($this->range)) { $results = array_slice($results, $this->range['start'], $this->range['length']); @@ -171,11 +187,24 @@ } } } - + // This will contain a multiple dimensional array. The first key will be a + // table alias, the second either result or all, the third will be a field + // alias. all contains every field in the table with the original field + // names while result contains only the fields requested. This allows for + // filtering on fields that were not added via addField(). $results = array(); foreach ($this->tables as $table_alias => $table_info) { - if (isset($table_info['join type'])) { + // The base table for this query. + if (empty($table_info['join type'])) { + foreach ($this->databaseContents[$table_info['table']] as $candidate_row) { + $results[] = $this->getNewRow($table_alias, $fields, $candidate_row); + } + } + else { $new_rows = array(); + + // Dynamically build a set of joined rows. Check existing rows and see + // if they can be joined with incoming rows. foreach ($results as $row) { $joined = FALSE; foreach ($this->databaseContents[$table_info['table']] as $candidate_row) { @@ -185,6 +214,9 @@ } } if (!$joined && $table_info['join type'] == 'LEFT') { + // Because PHP doesn't scope their foreach statements, + // $candidate_row may contain the last value assigned to it from the + // previous statement. // @TODO: empty tables? Those are a problem. $keys = array_keys($candidate_row); $values = array_fill(0, count($keys), NULL); @@ -197,11 +229,6 @@ } $results = $new_rows; } - else { - foreach ($this->databaseContents[$table_info['table']] as $candidate_row) { - $results[] = $this->getNewRow($table_alias, $fields, $candidate_row); - } - } } return $results; } @@ -244,10 +271,11 @@ * usort callback to order the results. */ protected function sortCallback($a, $b) { + $a_row = new DatabaseRowSelect($a, $this->fieldsWithTable, $this->fields); + $b_row = new DatabaseRowSelect($b, $this->fieldsWithTable, $this->fields); foreach ($this->order as $field => $direction) { - $field_info = $this->getFieldInfo($field); - $a_value = $this->getValue($a, $field_info); - $b_value = $this->getValue($b, $field_info); + $a_value = $a_row->getValue($field); + $b_value = $b_row->getValue($field); if ($a_value != $b_value) { return (($a_value < $b_value) == ($direction == 'ASC')) ? -1 : 1; } @@ -255,122 +283,21 @@ return 0; } - protected function getFieldInfo($field) { - return isset($this->fieldsWithTable[$field]) ? $this->fieldsWithTable[$field] : $this->fields[$field]; - } - /** * Resolves conditions by removing non-matching rows. * + * @param \Drupal\Core\Database\Query\Condition $condition_group + * The condition group to check. * @param array $rows * An array of rows excluding non-matching rows. */ protected function resolveConditions(Condition $condition_group, array &$rows) { - foreach ($rows as $k => $row) { - if (!$this->matchGroup($row, $condition_group)) { - unset($rows[$k]); - } - } - } - - /** - * Match a row against a group of conditions. - * - * @param array $row - * - * @param \Drupal\Core\Database\Query\Condition $condition_group - * - * @return bool - */ - protected function matchGroup(array $row, Condition $condition_group) { - $conditions = $condition_group->conditions(); - $and = $conditions['#conjunction'] == 'AND'; - unset($conditions['#conjunction']); - $match = TRUE; - foreach ($conditions as $condition) { - $match = $condition['field'] instanceof Condition ? $this->matchGroup($row, $condition['field']) : $this->matchSingle($row, $condition); - // For AND, finish matching on the first fail. For OR, finish on first - // success. - if ($and != $match) { - break; - } - } - return $match; - } - - /** - * Gets the value of a field from a row. - * - * @param $row - * The row array, three levels of indexes: first is the table alias, the - * second is either all or result, the third is the field alias. - * @param $field_info - * The field information array containing the table alias and the - * field alias. - * @return mixed - */ - protected function getValue($row, $field_info) { - if (array_key_exists($field_info['field'], $row[$field_info['table']]['result'])) { - $index = 'result'; - } - else { - $index = 'all'; - } - return $row[$field_info['table']][$index][$field_info['field']]; - } - - /** - * Match a single row and its condition. - * - * @param array $row - * The row to match. - * - * @param array $condition - * An array representing a single condition. - * - * @return bool - * TRUE if the condition matches. - * - * @throws \Exception - * - */ - protected function matchSingle(array $row, array $condition) { - $field_info = $this->getFieldInfo($condition['field']); - $row_value = $this->getValue($row, $field_info); - switch ($condition['operator']) { - case '=': - return $row_value == $condition['value']; - - case '<=': - return $row_value <= $condition['value']; - - case '>=': - return $row_value >= $condition['value']; - - case '!=': - return $row_value != $condition['value']; - - case '<>': - return $row_value != $condition['value']; - - case '<': - return $row_value < $condition['value']; - - case '>': - return $row_value > $condition['value']; - - case 'IN': - return in_array($row_value, $condition['value']); - - case 'IS NULL': - return !isset($row_value); - - case 'IS NOT NULL': - return isset($row_value); - - default: - throw new \Exception(sprintf('operator %s is not supported', $condition['operator'])); - } + $fields_with_table = $this->fieldsWithTable; + $fields = $this->fields; + return array_filter($rows, function ($row_array) use ($condition_group, $fields_with_table, $fields) { + $row = new DatabaseRowSelect($row_array, $fields_with_table, $fields); + return ConditionResolver::matchGroup($row, $condition_group); + }); } /** diff -u b/core/modules/migrate/tests/Drupal/migrate/Tests/MigrateSqlSourceTestCase.php b/core/modules/migrate/tests/Drupal/migrate/Tests/MigrateSqlSourceTestCase.php --- b/core/modules/migrate/tests/Drupal/migrate/Tests/MigrateSqlSourceTestCase.php +++ b/core/modules/migrate/tests/Drupal/migrate/Tests/MigrateSqlSourceTestCase.php @@ -7,8 +7,6 @@ namespace Drupal\migrate\Tests; -use Drupal\migrate\Source; - /** * Provides setup and helper methods for Migrate module source tests. */ @@ -23,12 +21,12 @@ protected $databaseContents = array(); - protected $results = array(); - const PLUGIN_CLASS = ''; const ORIGINAL_HIGHWATER = ''; + protected $expectedResults = array(); + /** * @var \Drupal\migrate\Plugin\MigrateSourceInterface */ @@ -38,17 +36,6 @@ * {@inheritdoc} */ protected function setUp() { - $database_contents = $this->databaseContents + array('test_map' => array()); - $database = $this->getMockBuilder('Drupal\Core\Database\Connection') - ->disableOriginalConstructor() - ->getMock(); - $database->expects($this->any())->method('select')->will($this->returnCallback(function ($base_table, $base_alias) use ($database_contents) { - return new FakeSelect($base_table, $base_alias, $database_contents); - })); - $database->expects($this->any())->method('schema')->will($this->returnCallback(function () use ($database, $database_contents) { - return new FakeDatabaseSchema($database, $database_contents); - })); - $database->expects($this->any())->method('query')->will($this->throwException(new \Exception('Query is not supported'))); $module_handler = $this->getMockBuilder('Drupal\Core\Extension\ModuleHandlerInterface') ->disableOriginalConstructor() ->getMock(); @@ -60,7 +47,7 @@ // Need the test class, not the original because we need a setDatabase method. This is not pretty :/ $plugin_class = preg_replace('/^(Drupal\\\\\w+\\\\)Plugin\\\\migrate(\\\\source(\\\\.+)?\\\\)([^\\\\]+)$/', '\1Tests\2Test\4', static::PLUGIN_CLASS); $plugin = new $plugin_class($this->migrationConfiguration['source'], $this->migrationConfiguration['source']['plugin'], array(), $migration); - $plugin->setDatabase($database); + $plugin->setDatabase($this->getDatabase($this->databaseContents + array('test_map' => array()))); $plugin->setModuleHandler($module_handler); $migration->expects($this->any()) ->method('getSourcePlugin') @@ -74,41 +61,17 @@ $this->source->setCache($cache); } - /** - * Tests retrieval. - */ public function testRetrieval() { - $this->assertSame(count($this->results), count($this->source), 'Number of results match'); - $count = 0; - foreach ($this->source as $data_row) { - $expected_row = $this->results[$count]; - $count++; - foreach ($expected_row as $key => $expected_value) { - $this->retrievalAssertHelper($expected_value, $data_row->getSourceProperty($key), sprintf('Value matches for key "%s"', $key)); - } - } - $this->assertSame(count($this->results), $count); + $this->queryResultTest($this->source, $this->expectedResults); } /** - * Asserts tested values during test retrieval. - * - * @param mixed $expected_value - * The incoming expected value to test. - * @param mixed $actual_value - * The incoming value itself. - * @param string $message - * The tested result as a formatted string. + * @param \Drupal\migrate\Row $row + * @param string $key + * @return mixed */ - protected function retrievalAssertHelper($expected_value, $actual_value, $message) { - if (is_array($expected_value)) { - foreach ($expected_value as $k => $v) { - $this->retrievalAssertHelper($v, $actual_value[$k], $message . '['. $k . ']'); - } - } - else { - $this->assertSame((string) $expected_value, (string) $actual_value, $message); - } + protected function getValue($row, $key) { + return $row->getSourceProperty($key); } /** diff -u b/core/modules/migrate/tests/Drupal/migrate/Tests/MigrateTestCase.php b/core/modules/migrate/tests/Drupal/migrate/Tests/MigrateTestCase.php --- b/core/modules/migrate/tests/Drupal/migrate/Tests/MigrateTestCase.php +++ b/core/modules/migrate/tests/Drupal/migrate/Tests/MigrateTestCase.php @@ -54,11 +54,88 @@ /** - * Provide meta information about this battery of tests. + * @return \Drupal\Core\Database\Connection */ - public static function getInfo() { - return array( - 'name' => 'Migrate test', - 'description' => 'Tests for migrate plugin.', - 'group' => 'Migrate', - ); + protected function getDatabase($database_contents) { + $database = $this->getMockBuilder('Drupal\Core\Database\Connection') + ->disableOriginalConstructor() + ->getMock(); + $database->databaseContents = &$database_contents; + $database->expects($this->any()) + ->method('select')->will($this->returnCallback(function ($base_table, $base_alias) use ($database_contents) { + return new FakeSelect($base_table, $base_alias, $database_contents); + })); + $database->expects($this->any()) + ->method('schema') + ->will($this->returnCallback(function () use (&$database_contents) { + return new FakeDatabaseSchema($database_contents); + })); + $database->expects($this->any()) + ->method('insert') + ->will($this->returnCallback(function ($table) use (&$database_contents) { + return new FakeInsert($database_contents, $table); + })); + $database->expects($this->any()) + ->method('update') + ->will($this->returnCallback(function ($table) use (&$database_contents) { + return new FakeUpdate($database_contents, $table); + })); + $database->expects($this->any()) + ->method('merge') + ->will($this->returnCallback(function ($table) use (&$database_contents) { + return new FakeMerge($database_contents, $table); + })); + $database->expects($this->any()) + ->method('query') + ->will($this->throwException(new \Exception('Query is not supported'))); + return $database; } + + /** + * Tests a query + * + * @param array|\Traversable + * The countable. foreach-able actual results if a query is being run. + */ + public function queryResultTest($iter, $expected_results) { + $this->assertSame(count($expected_results), count($iter), 'Number of results match'); + $count = 0; + foreach ($iter as $data_row) { + $expected_row = $expected_results[$count]; + $count++; + foreach ($expected_row as $key => $expected_value) { + $this->retrievalAssertHelper($expected_value, $this->getValue($data_row, $key), sprintf('Value matches for key "%s"', $key)); + } + } + $this->assertSame(count($expected_results), $count); + } + + /** + * @param array $row + * @param string $key + * @return mixed + */ + protected function getValue($row, $key) { + return $row[$key]; + } + + /** + * Asserts tested values during test retrieval. + * + * @param mixed $expected_value + * The incoming expected value to test. + * @param mixed $actual_value + * The incoming value itself. + * @param string $message + * The tested result as a formatted string. + */ + protected function retrievalAssertHelper($expected_value, $actual_value, $message) { + if (is_array($expected_value)) { + foreach ($expected_value as $k => $v) { + $this->retrievalAssertHelper($v, $actual_value[$k], $message . '['. $k . ']'); + } + } + else { + $this->assertSame((string) $expected_value, (string) $actual_value, $message); + } + } + } diff -u b/core/modules/migrate/tests/Drupal/migrate/Tests/process/GetTest.php b/core/modules/migrate/tests/Drupal/migrate/Tests/process/GetTest.php --- b/core/modules/migrate/tests/Drupal/migrate/Tests/process/GetTest.php +++ b/core/modules/migrate/tests/Drupal/migrate/Tests/process/GetTest.php @@ -7,32 +7,24 @@ namespace Drupal\migrate\Tests\process; use Drupal\migrate\Plugin\migrate\process\TestGet; -use Drupal\migrate\Tests\MigrateTestCase; -class GetTest extends MigrateTestCase { +/** + * @group migrate + */ +class GetTest extends MigrateProcessTestCase { /** - * @var \Drupal\migrate\Plugin\migrate\process\TestGet + * {@inheritdoc} */ - protected $plugin; - - /** - * @var \Drupal\migrate\Row - */ - protected $row; - - /** - * @var \Drupal\migrate\MigrateExecutable - */ - protected $migrateExecutable; + public static function getInfo() { + return array( + 'name' => 'Get process plugin', + 'description' => 'Tests the get process plugin.', + 'group' => 'Migrate', + ); + } function setUp() { - $this->row = $this->getMockBuilder('Drupal\migrate\Row') - ->disableOriginalConstructor() - ->getMock(); - $this->migrateExecutable = $this->getMockBuilder('Drupal\migrate\MigrateExecutable') - ->disableOriginalConstructor() - ->getMock(); $this->plugin = new TestGet(); parent::setUp(); } only in patch2: unchanged: --- /dev/null +++ b/core/modules/migrate/tests/Drupal/migrate/Tests/ConditionResolver.php @@ -0,0 +1,93 @@ +conditions(); + $and = $conditions['#conjunction'] == 'AND'; + unset($conditions['#conjunction']); + $match = TRUE; + foreach ($conditions as $condition) { + $match = $condition['field'] instanceof Condition ? static::matchGroup($row, $condition['field']) : static::matchSingle($row, $condition); + // For AND, finish matching on the first fail. For OR, finish on first + // success. + if ($and != $match) { + break; + } + } + return $match; + } + + /** + * Match a single row and its condition. + * + * @param \Drupal\migrate\tests\DatabaseRowInterface $row + * The row to match. + * + * @param array $condition + * An array representing a single condition. + * + * @return bool + * TRUE if the condition matches. + * + * @throws \Exception + * + */ + protected static function matchSingle(DatabaseRowInterface $row, array $condition) { + $row_value = $row->getValue($condition['field']); + switch ($condition['operator']) { + case '=': + return $row_value == $condition['value']; + + case '<=': + return $row_value <= $condition['value']; + + case '>=': + return $row_value >= $condition['value']; + + case '!=': + return $row_value != $condition['value']; + + case '<>': + return $row_value != $condition['value']; + + case '<': + return $row_value < $condition['value']; + + case '>': + return $row_value > $condition['value']; + + case 'IN': + return in_array($row_value, $condition['value']); + + case 'IS NULL': + return !isset($row_value); + + case 'IS NOT NULL': + return isset($row_value); + + default: + throw new \Exception(sprintf('operator %s is not supported', $condition['operator'])); + } + } + +} only in patch2: unchanged: --- /dev/null +++ b/core/modules/migrate/tests/Drupal/migrate/Tests/DatabaseRow.php @@ -0,0 +1,19 @@ +row = $row; + } + + public function getValue($field) { + return $this->row[$field]; + } +} only in patch2: unchanged: --- /dev/null +++ b/core/modules/migrate/tests/Drupal/migrate/Tests/DatabaseRowInterface.php @@ -0,0 +1,13 @@ +fieldsWithTable = $fieldsWithTable; + $this->fields = $fields; + parent::__construct($row); + } + + public function getValue($field) { + $field_info = isset($this->fieldsWithTable[$field]) ? $this->fieldsWithTable[$field] : $this->fields[$field]; + if (array_key_exists($field_info['field'], $this->row[$field_info['table']]['result'])) { + $index = 'result'; + } + else { + $index = 'all'; + } + return $this->row[$field_info['table']][$index][$field_info['field']]; + } +} only in patch2: unchanged: --- /dev/null +++ b/core/modules/migrate/tests/Drupal/migrate/Tests/process/MigrateProcessTestCase.php @@ -0,0 +1,38 @@ +row = $this->getMockBuilder('Drupal\migrate\Row') + ->disableOriginalConstructor() + ->getMock(); + $this->migrateExecutable = $this->getMockBuilder('Drupal\migrate\MigrateExecutable') + ->disableOriginalConstructor() + ->getMock(); + parent::setUp(); + } + +}