label 1 * column_index2 * property 2 => label 2 * updated * column_index1 * property 2 => label 2 * column_index2 * property 1 => label 1 * @endcode * * Example source configuration. * @code * custom_migration * original * 2 * title => title * 3 * body => foo * updated * 8 * title => new_title * 9 * body => new_body * @endcode */ class SourceCsvForm extends FormBase { /** * The database connection. * * @var \Drupal\Core\Database\Connection */ protected $connection; /** * Plugin manager for migration plugins. * * @var \Drupal\migrate\Plugin\MigrationPluginManagerInterface */ protected $migrationPluginManager; /** * The messenger service. * * @var \Drupal\Core\Messenger\MessengerInterface */ protected $messenger; /** * Temporary store for column assignment changes. * * @var \Drupal\Core\TempStore\PrivateTempStoreFactory */ protected $store; /** * The file object that reads the CSV file. * * @var \SplFileObject */ protected $file = NULL; /** * The migration being examined. * * @var \Drupal\migrate\Plugin\MigrationInterface */ protected $migration; /** * The migration plugin id. * * @var string */ protected $id; /** * The array of columns names from the CSV source plugin. * * @var array */ protected $columnNames; /** * An array of options for the column select form field.. * * @var array */ protected $options; /** * An array of modified and original column_name source plugin configuration. * * @var array */ protected $sourceConfiguration; /** * Constructs new SourceCsvForm object. * * @param \Drupal\Core\Database\Connection $connection * The database connection. * @param \Drupal\migrate\Plugin\MigrationPluginManagerInterface $migration_plugin_manager * The plugin manager for config entity-based migrations. * @param \Drupal\Core\Messenger\MessengerInterface $messenger * The messenger service. * @param \Drupal\Core\TempStore\PrivateTempStoreFactory $private_store * The private store. */ public function __construct(Connection $connection, MigrationPluginManagerInterface $migration_plugin_manager, MessengerInterface $messenger, PrivateTempStoreFactory $private_store) { $this->connection = $connection; $this->migrationPluginManager = $migration_plugin_manager; $this->messenger = $messenger; $this->store = $private_store->get('migrate_tools'); } /** * {@inheritdoc} */ public static function create(ContainerInterface $container) { return new static( $container->get('database'), $container->get('plugin.manager.migration'), $container->get('messenger'), $container->get('tempstore.private') ); } /** * A custom access check. * * @param \Drupal\Core\Session\AccountInterface $account * Run access checks for this account. * @param \Drupal\migrate_plus\Entity\MigrationInterface $migration * The $migration. * * @return \Drupal\Core\Access\AccessResult * Allowed or forbidden, neutral if tempstore is empty. */ public function access(AccountInterface $account, MigrationInterface $migration) { try { $this->migration = $this->migrationPluginManager->createInstance($migration->id(), $migration->toArray()); } catch (PluginException $e) { return AccessResult::forbidden(); } if ($this->migration) { if ($source = $this->migration->getSourcePlugin()) { if (is_a($source, CSV::class)) { return AccessResult::allowed(); } } } return AccessResult::forbidden(); } /** * {@inheritdoc} */ public function buildForm(array $form, FormStateInterface $form_state, MigrationInterface $migration = NULL) { try { // @TODO: remove this horrible config work around after // https://www.drupal.org/project/drupal/issues/2986665 is fixed. $this->migration = $this->migrationPluginManager->createInstance($migration->id(), $migration->toArray()); /** @var \Drupal\migrate_source_csv\Plugin\migrate\source\CSV $source */ $source = $this->migration->getSourcePlugin(); $source->setConfiguration($migration->toArray()['source']); } catch (PluginException $e) { return AccessResult::forbidden(); } // Get the source file after the properties are initialized. $source->initializeIterator(); $this->file = $source->getFile(); // Set the input field options to the header row values or, if there are // no such values, use an indexed array. if ($this->file->getHeaderRowCount() > 0) { $this->options = $this->getHeaderColumnNames(); } else { for ($i = 0; $i < $this->getFileColumnCount(); $i++) { $this->options[$i] = $i; } } // Set the store key to the migration id. $this->id = $this->migration->getPluginId(); // Get the column names from the file or from the store, if updated // values are in the store. $this->sourceConfiguration = $this->store->get($this->id); if (isset($this->sourceConfiguration['changed'])) { if ($config = $this->sourceConfiguration['changed']) { $this->columnNames = $config; } } else { // Get the calculated column names. This is either the header rows or // the configuration column_name value. $this->columnNames = $this->file->getColumnNames(); if (!isset($this->sourceConfiguration['original'])) { // Save as the original values. $this->sourceConfiguration['original'] = $this->columnNames; $this->store->set($this->id, $this->sourceConfiguration); } } $form['#title'] = $this->t('Column Aliases'); $form['heading'] = [ '#type' => 'item', '#title' => $this->t(':label', [':label' => $this->migration->label()]), '#description' => '

' . $this->t('You can change the columns to be used by this migration for each source property.') . '

', ]; // Create a form field for each column in this migration. foreach ($this->columnNames as $index => $data) { $property_name = key($data); $default_value = $index; $label = $this->getLabel($this->sourceConfiguration['original'], $property_name); $description = $this->t('Select the column where the data for :label, property :property, will be found.', [ ':label' => $label, ':property' => $property_name, ]); $form['aliases'][$property_name] = [ '#type' => 'select', '#title' => $label, '#description' => $description, '#options' => $this->options, '#default_value' => $default_value, ]; } $form['actions'] = ['#type' => 'actions']; $form['actions']['submit'] = [ '#type' => 'submit', '#button_type' => 'primary', '#value' => $this->t('Submit'), ]; $form['actions']['cancel'] = [ '#type' => 'submit', '#value' => $this->t('Cancel'), '#submit' => ['::cancel'], '#limit_validation_errors' => [], ]; return $form; } /** * {@inheritdoc} */ public function validateForm(array &$form, FormStateInterface $form_state) { // Display an error message if two properties have the same source column. $values = []; foreach ($this->columnNames as $index => $data) { $property_name = key($data); $value = $form_state->getValue($property_name); if (in_array($value, $values)) { $form_state->setErrorByName($property_name, $this->t('Source properties can not share the same source column.')); } $values[] = $value; } } /** * {@inheritdoc} */ public function submitForm(array &$form, FormStateInterface $form_state) { // Create a new column_names configuration. $new_column_names = []; foreach ($this->columnNames as $index => $data) { // Keep the property name as it is used in the process pipeline. $property_name = key($data); // Get the new column number from the form alias field for this property. $new_index = $form_state->getValue($property_name); // Get the new label from the options array. $new_label = $this->options[$new_index]; // Save using the new column number and new label. $new_column_names[$new_index] = [$property_name => $new_label]; } // Update the file columns. $this->file->setColumnNames($new_column_names); // Save as updated in the store. $this->sourceConfiguration['changed'] = $new_column_names; $this->store->set($this->id, $this->sourceConfiguration); $changed = ($this->store->get('migrations_changed')) ? $this->store->get('migrations_changed') : []; if (!in_array($this->id, $changed)) { $changed[] = $this->id; $this->store->set('migrations_changed', $changed); } } /** * Form submission handler for the 'cancel' action. * * @param array $form * An associative array containing the structure of the form. * @param \Drupal\Core\Form\FormStateInterface $form_state * The current state of the form. */ public function cancel(array $form, FormStateInterface $form_state) { // Restore the file columns to the original settings. $this->file->setColumnNames($this->sourceConfiguration['original']); // Remove this migration from the store. try { $this->store->delete($this->id); } catch (TempStoreException $e) { $this->messenger->addError($e->getMessage()); } $migrationsChanged = $this->store->get('migrations_changed'); unset($migrationsChanged[$this->id]); try { $this->store->set('migrations_changed', $migrationsChanged); } catch (TempStoreException $e) { $this->messenger->addError($e->getMessage()); } $form_state->setRedirect('entity.migration_group.list'); } /** * {@inheritdoc} */ public function getFormId() { return 'migrate_tools_source_csv'; } /** * {@inheritdoc} */ public function getQuestion() {} /** * {@inheritdoc} */ public function getCancelUrl() { return new Url('entity.migration_group.list'); } /** * Returns the header row. * * Use a new file handle so that CSVFileObject::current() is not executed. * * @return array * The header row. */ public function getHeaderColumnNames() { $row = []; $fname = $this->file->getPathname(); $handle = fopen($fname, 'r'); if ($handle) { fseek($handle, $this->file->getHeaderRowCount() - 1); $row = fgetcsv($handle); fclose($handle); } return $row; } /** * Returns the count of fields in the header row. * * Use a new file handle so that CSVFileObject::current() is not executed. * * @return int * The number of fields in the header row. */ public function getFileColumnCount() { $count = 0; $fname = $this->file->getPathname(); $handle = fopen($fname, 'r'); if ($handle) { $row = fgetcsv($handle); $count = count($row); fclose($handle); } return $count; } /** * Gets the label for a given property from a column_names array. * * @param array $column_names * An array of column_names. * @param string $property_name * The property name to find a label for. * * @return string * The label for this property. */ protected function getLabel(array $column_names, $property_name) { $label = ''; foreach ($column_names as $column) { foreach ($column as $key => $value) { if ($key === $property_name) { $label = $value; break; } } } return $label; } }