Updated all the contrib modules to their latest versions.
[yaffs-website] / web / modules / contrib / migrate_tools / src / Form / SourceCsvForm.php
1 <?php
2
3 namespace Drupal\migrate_tools\Form;
4
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;
14 use Drupal\Core\Url;
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;
19
20 /**
21  * Provides an edit form for CSV source plugin column_names configuration.
22  *
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.
26  *
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.
31  *
32  * An addtional list of all changed migration id is kept in the store, in the
33  * key 'migrations_changed'
34  *
35  * Private Store Usage:
36  *   migrations_changed: An array of the ids of the migrations that have been
37  * changed:
38  *   [migration_id]: The original and changed values for this column assignments
39  *
40  * Format of the source configuration saved in the store.
41  * @code
42  * migration_id
43  *   original
44  *     column_index1
45  *       property 1 => label 1
46  *     column_index2
47  *       property 2 => label 2
48  *   updated
49  *     column_index1
50  *       property 2 => label 2
51  *     column_index2
52  *       property 1 => label 1
53  * @endcode
54  *
55  * Example source configuration.
56  * @code
57  * custom_migration
58  *  original
59  *   2
60  *     title => title
61  *   3
62  *     body => foo
63  *  updated
64  *   8
65  *     title => new_title
66  *   9
67  *     body => new_body
68  * @endcode
69  */
70 class SourceCsvForm extends FormBase {
71
72   /**
73    * The database connection.
74    *
75    * @var \Drupal\Core\Database\Connection
76    */
77   protected $connection;
78
79   /**
80    * Plugin manager for migration plugins.
81    *
82    * @var \Drupal\migrate\Plugin\MigrationPluginManagerInterface
83    */
84   protected $migrationPluginManager;
85
86   /**
87    * The messenger service.
88    *
89    * @var \Drupal\Core\Messenger\MessengerInterface
90    */
91   protected $messenger;
92
93   /**
94    * Temporary store for column assignment changes.
95    *
96    * @var \Drupal\Core\TempStore\PrivateTempStoreFactory
97    */
98   protected $store;
99
100   /**
101    * The file object that reads the CSV file.
102    *
103    * @var \SplFileObject
104    */
105   protected $file = NULL;
106
107   /**
108    * The migration being examined.
109    *
110    * @var \Drupal\migrate\Plugin\MigrationInterface
111    */
112   protected $migration;
113
114   /**
115    * The migration plugin id.
116    *
117    * @var string
118    */
119   protected $id;
120
121   /**
122    * The array of columns names from the CSV source plugin.
123    *
124    * @var array
125    */
126   protected $columnNames;
127
128   /**
129    * An array of options for the column select form field..
130    *
131    * @var array
132    */
133   protected $options;
134
135   /**
136    * An array of modified and original column_name source plugin configuration.
137    *
138    * @var array
139    */
140   protected $sourceConfiguration;
141
142   /**
143    * Constructs new SourceCsvForm object.
144    *
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
152    *   The private store.
153    */
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');
159   }
160
161   /**
162    * {@inheritdoc}
163    */
164   public static function create(ContainerInterface $container) {
165     return new static(
166       $container->get('database'),
167       $container->get('plugin.manager.migration'),
168       $container->get('messenger'),
169       $container->get('tempstore.private')
170     );
171   }
172
173   /**
174    * A custom access check.
175    *
176    * @param \Drupal\Core\Session\AccountInterface $account
177    *   Run access checks for this account.
178    * @param \Drupal\migrate_plus\Entity\MigrationInterface $migration
179    *   The $migration.
180    *
181    * @return \Drupal\Core\Access\AccessResult
182    *   Allowed or forbidden, neutral if tempstore is empty.
183    */
184   public function access(AccountInterface $account, MigrationInterface $migration) {
185     try {
186       $this->migration = $this->migrationPluginManager->createInstance($migration->id(), $migration->toArray());
187     }
188     catch (PluginException $e) {
189       return AccessResult::forbidden();
190     }
191
192     if ($this->migration) {
193       if ($source = $this->migration->getSourcePlugin()) {
194         if (is_a($source, CSV::class)) {
195           return AccessResult::allowed();
196         }
197       }
198     }
199     return AccessResult::forbidden();
200   }
201
202   /**
203    * {@inheritdoc}
204    */
205   public function buildForm(array $form, FormStateInterface $form_state, MigrationInterface $migration = NULL) {
206     try {
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']);
213     }
214     catch (PluginException $e) {
215       return AccessResult::forbidden();
216     }
217
218     // Get the source file after the properties are initialized.
219     $source->initializeIterator();
220     $this->file = $source->getFile();
221
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();
226     }
227     else {
228       for ($i = 0; $i < $this->getFileColumnCount(); $i++) {
229         $this->options[$i] = $i;
230       }
231     }
232
233     // Set the store key to the migration id.
234     $this->id = $this->migration->getPluginId();
235
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;
242       }
243     }
244     else {
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);
252       }
253     }
254     $form['#title'] = $this->t('Column Aliases');
255
256     $form['heading'] = [
257       '#type' => 'item',
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>',
260     ];
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);
266
267       $description = $this->t('Select the column where the data for <em>:label</em>, property <em>:property</em>, will be found.', [
268         ':label' => $label,
269         ':property' => $property_name,
270       ]);
271       $form['aliases'][$property_name] = [
272         '#type' => 'select',
273         '#title' => $label,
274         '#description' => $description,
275         '#options' => $this->options,
276         '#default_value' => $default_value,
277       ];
278     }
279     $form['actions'] = ['#type' => 'actions'];
280     $form['actions']['submit'] = [
281       '#type' => 'submit',
282       '#button_type' => 'primary',
283       '#value' => $this->t('Submit'),
284     ];
285     $form['actions']['cancel'] = [
286       '#type' => 'submit',
287       '#value' => $this->t('Cancel'),
288       '#submit' => ['::cancel'],
289       '#limit_validation_errors' => [],
290     ];
291
292     return $form;
293   }
294
295   /**
296    * {@inheritdoc}
297    */
298   public function validateForm(array &$form, FormStateInterface $form_state) {
299     // Display an error message if two properties have the same source column.
300     $values = [];
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.'));
306       }
307       $values[] = $value;
308     }
309   }
310
311   /**
312    * {@inheritdoc}
313    */
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];
326     }
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);
332
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);
337     }
338   }
339
340   /**
341    * Form submission handler for the 'cancel' action.
342    *
343    * @param array $form
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.
347    */
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.
352     try {
353       $this->store->delete($this->id);
354     }
355     catch (TempStoreException $e) {
356       $this->messenger->addError($e->getMessage());
357     }
358
359     $migrationsChanged = $this->store->get('migrations_changed');
360     unset($migrationsChanged[$this->id]);
361     try {
362       $this->store->set('migrations_changed', $migrationsChanged);
363     }
364     catch (TempStoreException $e) {
365       $this->messenger->addError($e->getMessage());
366     }
367     $form_state->setRedirect('entity.migration_group.list');
368   }
369
370   /**
371    * {@inheritdoc}
372    */
373   public function getFormId() {
374     return 'migrate_tools_source_csv';
375   }
376
377   /**
378    * {@inheritdoc}
379    */
380   public function getQuestion() {}
381
382   /**
383    * {@inheritdoc}
384    */
385   public function getCancelUrl() {
386     return new Url('entity.migration_group.list');
387   }
388
389   /**
390    * Returns the header row.
391    *
392    * Use a new file handle so that CSVFileObject::current() is not executed.
393    *
394    * @return array
395    *   The header row.
396    */
397   public function getHeaderColumnNames() {
398     $row = [];
399     $fname = $this->file->getPathname();
400     $handle = fopen($fname, 'r');
401     if ($handle) {
402       fseek($handle, $this->file->getHeaderRowCount() - 1);
403       $row = fgetcsv($handle);
404       fclose($handle);
405     }
406     return $row;
407   }
408
409   /**
410    * Returns the count of fields in the header row.
411    *
412    * Use a new file handle so that CSVFileObject::current() is not executed.
413    *
414    * @return int
415    *   The number of fields in the header row.
416    */
417   public function getFileColumnCount() {
418     $count = 0;
419     $fname = $this->file->getPathname();
420     $handle = fopen($fname, 'r');
421     if ($handle) {
422       $row = fgetcsv($handle);
423       $count = count($row);
424       fclose($handle);
425     }
426     return $count;
427   }
428
429   /**
430    * Gets the label for a given property from a column_names array.
431    *
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.
436    *
437    * @return string
438    *   The label for this property.
439    */
440   protected function getLabel(array $column_names, $property_name) {
441     $label = '';
442     foreach ($column_names as $column) {
443       foreach ($column as $key => $value) {
444         if ($key === $property_name) {
445           $label = $value;
446           break;
447         }
448       }
449     }
450     return $label;
451   }
452
453 }