3 namespace Drupal\migrate_tools\Form;
5 use Drupal\Component\Plugin\Exception\PluginException;
6 use Drupal\Core\Access\AccessResult;
7 use Drupal\Core\Session\AccountInterface;
8 use Drupal\Core\Database\Connection;
9 use Drupal\Core\Form\FormBase;
10 use Drupal\Core\Form\FormStateInterface;
11 use Drupal\Core\Messenger\MessengerInterface;
12 use Drupal\Core\TempStore\PrivateTempStoreFactory;
13 use Drupal\Core\TempStore\TempStoreException;
15 use Drupal\migrate_plus\Entity\MigrationInterface;
16 use Drupal\migrate_source_csv\Plugin\migrate\source\CSV;
17 use Symfony\Component\DependencyInjection\ContainerInterface;
18 use Drupal\migrate\Plugin\MigrationPluginManagerInterface;
21 * Provides an edit form for CSV source plugin column_names configuration.
23 * This means you can tell the migration which columns your data is in and no
24 * longer edit the CSV to fit the column order set in the migration or edit the
25 * migration yml itself.
27 * Changes made to the column configuration, or aliases, are stored in the
28 * private migrate_toools private store keyed by the migration plugin id. The
29 * data stored for each migrations consists of two arrays, the 'original' column
30 * aliases and the 'updated' column aliases.
32 * An addtional list of all changed migration id is kept in the store, in the
33 * key 'migrations_changed'
35 * Private Store Usage:
36 * migrations_changed: An array of the ids of the migrations that have been
38 * [migration_id]: The original and changed values for this column assignments
40 * Format of the source configuration saved in the store.
45 * property 1 => label 1
47 * property 2 => label 2
50 * property 2 => label 2
52 * property 1 => label 1
55 * Example source configuration.
70 class SourceCsvForm extends FormBase {
73 * The database connection.
75 * @var \Drupal\Core\Database\Connection
77 protected $connection;
80 * Plugin manager for migration plugins.
82 * @var \Drupal\migrate\Plugin\MigrationPluginManagerInterface
84 protected $migrationPluginManager;
87 * The messenger service.
89 * @var \Drupal\Core\Messenger\MessengerInterface
94 * Temporary store for column assignment changes.
96 * @var \Drupal\Core\TempStore\PrivateTempStoreFactory
101 * The file object that reads the CSV file.
103 * @var \SplFileObject
105 protected $file = NULL;
108 * The migration being examined.
110 * @var \Drupal\migrate\Plugin\MigrationInterface
112 protected $migration;
115 * The migration plugin id.
122 * The array of columns names from the CSV source plugin.
126 protected $columnNames;
129 * An array of options for the column select form field..
136 * An array of modified and original column_name source plugin configuration.
140 protected $sourceConfiguration;
143 * Constructs new SourceCsvForm object.
145 * @param \Drupal\Core\Database\Connection $connection
146 * The database connection.
147 * @param \Drupal\migrate\Plugin\MigrationPluginManagerInterface $migration_plugin_manager
148 * The plugin manager for config entity-based migrations.
149 * @param \Drupal\Core\Messenger\MessengerInterface $messenger
150 * The messenger service.
151 * @param \Drupal\Core\TempStore\PrivateTempStoreFactory $private_store
154 public function __construct(Connection $connection, MigrationPluginManagerInterface $migration_plugin_manager, MessengerInterface $messenger, PrivateTempStoreFactory $private_store) {
155 $this->connection = $connection;
156 $this->migrationPluginManager = $migration_plugin_manager;
157 $this->messenger = $messenger;
158 $this->store = $private_store->get('migrate_tools');
164 public static function create(ContainerInterface $container) {
166 $container->get('database'),
167 $container->get('plugin.manager.migration'),
168 $container->get('messenger'),
169 $container->get('tempstore.private')
174 * A custom access check.
176 * @param \Drupal\Core\Session\AccountInterface $account
177 * Run access checks for this account.
178 * @param \Drupal\migrate_plus\Entity\MigrationInterface $migration
181 * @return \Drupal\Core\Access\AccessResult
182 * Allowed or forbidden, neutral if tempstore is empty.
184 public function access(AccountInterface $account, MigrationInterface $migration) {
186 $this->migration = $this->migrationPluginManager->createInstance($migration->id(), $migration->toArray());
188 catch (PluginException $e) {
189 return AccessResult::forbidden();
192 if ($this->migration) {
193 if ($source = $this->migration->getSourcePlugin()) {
194 if (is_a($source, CSV::class)) {
195 return AccessResult::allowed();
199 return AccessResult::forbidden();
205 public function buildForm(array $form, FormStateInterface $form_state, MigrationInterface $migration = NULL) {
207 // @TODO: remove this horrible config work around after
208 // https://www.drupal.org/project/drupal/issues/2986665 is fixed.
209 $this->migration = $this->migrationPluginManager->createInstance($migration->id(), $migration->toArray());
210 /** @var \Drupal\migrate_source_csv\Plugin\migrate\source\CSV $source */
211 $source = $this->migration->getSourcePlugin();
212 $source->setConfiguration($migration->toArray()['source']);
214 catch (PluginException $e) {
215 return AccessResult::forbidden();
218 // Get the source file after the properties are initialized.
219 $source->initializeIterator();
220 $this->file = $source->getFile();
222 // Set the input field options to the header row values or, if there are
223 // no such values, use an indexed array.
224 if ($this->file->getHeaderRowCount() > 0) {
225 $this->options = $this->getHeaderColumnNames();
228 for ($i = 0; $i < $this->getFileColumnCount(); $i++) {
229 $this->options[$i] = $i;
233 // Set the store key to the migration id.
234 $this->id = $this->migration->getPluginId();
236 // Get the column names from the file or from the store, if updated
237 // values are in the store.
238 $this->sourceConfiguration = $this->store->get($this->id);
239 if (isset($this->sourceConfiguration['changed'])) {
240 if ($config = $this->sourceConfiguration['changed']) {
241 $this->columnNames = $config;
245 // Get the calculated column names. This is either the header rows or
246 // the configuration column_name value.
247 $this->columnNames = $this->file->getColumnNames();
248 if (!isset($this->sourceConfiguration['original'])) {
249 // Save as the original values.
250 $this->sourceConfiguration['original'] = $this->columnNames;
251 $this->store->set($this->id, $this->sourceConfiguration);
254 $form['#title'] = $this->t('Column Aliases');
258 '#title' => $this->t(':label', [':label' => $this->migration->label()]),
259 '#description' => '<p>' . $this->t('You can change the columns to be used by this migration for each source property.') . '</p>',
261 // Create a form field for each column in this migration.
262 foreach ($this->columnNames as $index => $data) {
263 $property_name = key($data);
264 $default_value = $index;
265 $label = $this->getLabel($this->sourceConfiguration['original'], $property_name);
267 $description = $this->t('Select the column where the data for <em>:label</em>, property <em>:property</em>, will be found.', [
269 ':property' => $property_name,
271 $form['aliases'][$property_name] = [
274 '#description' => $description,
275 '#options' => $this->options,
276 '#default_value' => $default_value,
279 $form['actions'] = ['#type' => 'actions'];
280 $form['actions']['submit'] = [
282 '#button_type' => 'primary',
283 '#value' => $this->t('Submit'),
285 $form['actions']['cancel'] = [
287 '#value' => $this->t('Cancel'),
288 '#submit' => ['::cancel'],
289 '#limit_validation_errors' => [],
298 public function validateForm(array &$form, FormStateInterface $form_state) {
299 // Display an error message if two properties have the same source column.
301 foreach ($this->columnNames as $index => $data) {
302 $property_name = key($data);
303 $value = $form_state->getValue($property_name);
304 if (in_array($value, $values)) {
305 $form_state->setErrorByName($property_name, $this->t('Source properties can not share the same source column.'));
314 public function submitForm(array &$form, FormStateInterface $form_state) {
315 // Create a new column_names configuration.
316 $new_column_names = [];
317 foreach ($this->columnNames as $index => $data) {
318 // Keep the property name as it is used in the process pipeline.
319 $property_name = key($data);
320 // Get the new column number from the form alias field for this property.
321 $new_index = $form_state->getValue($property_name);
322 // Get the new label from the options array.
323 $new_label = $this->options[$new_index];
324 // Save using the new column number and new label.
325 $new_column_names[$new_index] = [$property_name => $new_label];
327 // Update the file columns.
328 $this->file->setColumnNames($new_column_names);
329 // Save as updated in the store.
330 $this->sourceConfiguration['changed'] = $new_column_names;
331 $this->store->set($this->id, $this->sourceConfiguration);
333 $changed = ($this->store->get('migrations_changed')) ? $this->store->get('migrations_changed') : [];
334 if (!in_array($this->id, $changed)) {
335 $changed[] = $this->id;
336 $this->store->set('migrations_changed', $changed);
341 * Form submission handler for the 'cancel' action.
344 * An associative array containing the structure of the form.
345 * @param \Drupal\Core\Form\FormStateInterface $form_state
346 * The current state of the form.
348 public function cancel(array $form, FormStateInterface $form_state) {
349 // Restore the file columns to the original settings.
350 $this->file->setColumnNames($this->sourceConfiguration['original']);
351 // Remove this migration from the store.
353 $this->store->delete($this->id);
355 catch (TempStoreException $e) {
356 $this->messenger->addError($e->getMessage());
359 $migrationsChanged = $this->store->get('migrations_changed');
360 unset($migrationsChanged[$this->id]);
362 $this->store->set('migrations_changed', $migrationsChanged);
364 catch (TempStoreException $e) {
365 $this->messenger->addError($e->getMessage());
367 $form_state->setRedirect('entity.migration_group.list');
373 public function getFormId() {
374 return 'migrate_tools_source_csv';
380 public function getQuestion() {}
385 public function getCancelUrl() {
386 return new Url('entity.migration_group.list');
390 * Returns the header row.
392 * Use a new file handle so that CSVFileObject::current() is not executed.
397 public function getHeaderColumnNames() {
399 $fname = $this->file->getPathname();
400 $handle = fopen($fname, 'r');
402 fseek($handle, $this->file->getHeaderRowCount() - 1);
403 $row = fgetcsv($handle);
410 * Returns the count of fields in the header row.
412 * Use a new file handle so that CSVFileObject::current() is not executed.
415 * The number of fields in the header row.
417 public function getFileColumnCount() {
419 $fname = $this->file->getPathname();
420 $handle = fopen($fname, 'r');
422 $row = fgetcsv($handle);
423 $count = count($row);
430 * Gets the label for a given property from a column_names array.
432 * @param array $column_names
433 * An array of column_names.
434 * @param string $property_name
435 * The property name to find a label for.
438 * The label for this property.
440 protected function getLabel(array $column_names, $property_name) {
442 foreach ($column_names as $column) {
443 foreach ($column as $key => $value) {
444 if ($key === $property_name) {