You are here

class SourceCsvForm in Migrate Tools 8.4

Provides an edit form for CSV source plugin column_names configuration.

This means you can tell the migration which columns your data is in and no longer edit the CSV to fit the column order set in the migration or edit the migration yml itself.

Changes made to the column configuration, or aliases, are stored in the private migrate_tools private store keyed by the migration plugin id. The data stored for each migrations consists of two arrays, the 'original' column aliases and the 'updated' column aliases.

An additional list of all changed migration id is kept in the store, in the key 'migrations_changed'

Private Store Usage: migrations_changed: An array of the ids of the migrations that have been changed: [migration_id]: The original and changed values for this column assignments

Format of the source configuration saved in the store. migration_id original column_index1 property 1 => label 1 column_index2 property 2 => label 2 updated column_index1 property 2 => label 2 column_index2 property 1 => label 1

Example source configuration: custom_migration original 2 title => title 3 body => foo updated 8 title => new_title 9 body => new_body

Hierarchy

Expanded class hierarchy of SourceCsvForm

1 string reference to 'SourceCsvForm'
migrate_tools.routing.yml in ./migrate_tools.routing.yml
migrate_tools.routing.yml

File

src/Form/SourceCsvForm.php, line 66

Namespace

Drupal\migrate_tools\Form
View source
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' => '<p>' . $this
        ->t('You can change the columns to be used by this migration for each source property.') . '</p>',
    ];

    // 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 <em>:label</em>, property <em>:property</em>, 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;
  }

}

Members

Namesort descending Modifiers Type Description Overrides
DependencySerializationTrait::$_entityStorages protected property An array of entity type IDs keyed by the property name of their storages.
DependencySerializationTrait::$_serviceIds protected property An array of service IDs keyed by property name used for serialization.
DependencySerializationTrait::__sleep public function 1
DependencySerializationTrait::__wakeup public function 2
FormBase::$configFactory protected property The config factory. 1
FormBase::$requestStack protected property The request stack. 1
FormBase::$routeMatch protected property The route match.
FormBase::config protected function Retrieves a configuration object.
FormBase::configFactory protected function Gets the config factory for this form. 1
FormBase::container private function Returns the service container.
FormBase::currentUser protected function Gets the current user.
FormBase::getRequest protected function Gets the request object.
FormBase::getRouteMatch protected function Gets the route match.
FormBase::logger protected function Gets the logger for a specific channel.
FormBase::redirect protected function Returns a redirect response object for the specified route. Overrides UrlGeneratorTrait::redirect
FormBase::resetConfigFactory public function Resets the configuration factory.
FormBase::setConfigFactory public function Sets the config factory for this form.
FormBase::setRequestStack public function Sets the request stack object to use.
LinkGeneratorTrait::$linkGenerator protected property The link generator. 1
LinkGeneratorTrait::getLinkGenerator Deprecated protected function Returns the link generator.
LinkGeneratorTrait::l Deprecated protected function Renders a link to a route given a route name and its parameters.
LinkGeneratorTrait::setLinkGenerator Deprecated public function Sets the link generator service.
LoggerChannelTrait::$loggerFactory protected property The logger channel factory service.
LoggerChannelTrait::getLogger protected function Gets the logger for a specific channel.
LoggerChannelTrait::setLoggerFactory public function Injects the logger channel factory.
MessengerTrait::messenger public function Gets the messenger. 29
MessengerTrait::setMessenger public function Sets the messenger.
RedirectDestinationTrait::$redirectDestination protected property The redirect destination service. 1
RedirectDestinationTrait::getDestinationArray protected function Prepares a 'destination' URL query parameter for use with \Drupal\Core\Url.
RedirectDestinationTrait::getRedirectDestination protected function Returns the redirect destination service.
RedirectDestinationTrait::setRedirectDestination public function Sets the redirect destination service.
SourceCsvForm::$columnNames protected property The array of columns names from the CSV source plugin.
SourceCsvForm::$connection protected property The database connection.
SourceCsvForm::$file protected property The file object that reads the CSV file.
SourceCsvForm::$id protected property The migration plugin id.
SourceCsvForm::$messenger protected property The messenger service. Overrides MessengerTrait::$messenger
SourceCsvForm::$migration protected property The migration being examined.
SourceCsvForm::$migrationPluginManager protected property Plugin manager for migration plugins.
SourceCsvForm::$options protected property An array of options for the column select form field..
SourceCsvForm::$sourceConfiguration protected property An array of modified and original column_name source plugin configuration.
SourceCsvForm::$store protected property Temporary store for column assignment changes.
SourceCsvForm::access public function A custom access check.
SourceCsvForm::buildForm public function Form constructor. Overrides FormInterface::buildForm
SourceCsvForm::cancel public function Form submission handler for the 'cancel' action.
SourceCsvForm::create public static function Instantiates a new instance of this class. Overrides FormBase::create
SourceCsvForm::getCancelUrl public function
SourceCsvForm::getFileColumnCount public function Returns the count of fields in the header row.
SourceCsvForm::getFormId public function Returns a unique string identifying the form. Overrides FormInterface::getFormId
SourceCsvForm::getHeaderColumnNames public function Returns the header row.
SourceCsvForm::getLabel protected function Gets the label for a given property from a column_names array.
SourceCsvForm::getQuestion public function
SourceCsvForm::submitForm public function Form submission handler. Overrides FormInterface::submitForm
SourceCsvForm::validateForm public function Form validation handler. Overrides FormBase::validateForm
SourceCsvForm::__construct public function Constructs new SourceCsvForm object.
StringTranslationTrait::$stringTranslation protected property The string translation service. 1
StringTranslationTrait::formatPlural protected function Formats a string containing a count of items.
StringTranslationTrait::getNumberOfPlurals protected function Returns the number of plurals supported by a given language.
StringTranslationTrait::getStringTranslation protected function Gets the string translation service.
StringTranslationTrait::setStringTranslation public function Sets the string translation service to use. 2
StringTranslationTrait::t protected function Translates a string to the current language or to a given language.
UrlGeneratorTrait::$urlGenerator protected property The url generator.
UrlGeneratorTrait::getUrlGenerator Deprecated protected function Returns the URL generator service.
UrlGeneratorTrait::setUrlGenerator Deprecated public function Sets the URL generator service.
UrlGeneratorTrait::url Deprecated protected function Generates a URL or path for a specific route based on the given parameters.