cforge-2.0.x-dev/modules/cforge_import/src/ImportForm.php
modules/cforge_import/src/ImportForm.php
<?php
namespace Drupal\cforge_import;
// This is for the validation of large sets.
ini_set('max_execution_time', 200);
use Symfony\Component\DependencyInjection\ContainerInterface;
use Drupal\Core\Form\FormBase;
use Drupal\Core\Form\FormStateInterface;
use Drupal\cforge_import\CsvImportManager;
/**
* Form builder for selecting and importing from a csv.
*/
class ImportForm extends FormBase {
private $csvImportManager;
private $plugin;
/**
* Constructor.
*/
public function __construct(CsvImportManager $csv_import_manager) {
$this->csvImporter = $csv_import_manager;
}
/**
* {@inheritDoc}
*/
public static function create(ContainerInterface $container) {
return new static(
$container->get('cforge.csv_importer')
);
}
/**
* {@inheritDoc}
*/
public function getFormId() {
return 'cforge_import_csv_form';
}
/**
* {@inheritDoc}
*/
public function buildForm(array $form, FormStateInterface $form_state) {
$form['format'] = [
'#title' => $this->t('Csv source'),
'#type' => 'radios',
'#options' => [
'file' => $this->t('Upload file'),
'paste' => $this->t('Paste csv text'),
'url' => $this->t('Absolute url'),
],
'#default_value' => isset($_SESSION['import']) ? $_SESSION['import']['format'] : '',
];
$form['import_type'] = [
'#title' => $this->t('Data type'),
'#descriptions' => $this->t('Importable types'),
'#type' => 'radios',
'#weight' => 1,
];
$options = [];
foreach ($this->getImportPluginDefs() as $id => $def) {
$form['import_type']['#options'][$id] = $def['label'];
$this->setPlugin($id);
$col_names = array_keys($this->plugin->columns());
$description = $this->t(
'Csv columns: @columns',
['@columns' => implode(', ', $col_names)]
);
if ($this->plugin->ready()) {
$form[$id . '_info'] = [
'#type' => 'item',
'#markup' => $this->t('Field descriptions') . '<br />',
'#states' => [
'visible' => [
':input[name="import_type"]' => ['value' => "$id"],
],
],
'#weight' => 4,
];
foreach ($this->plugin->columns() as $field => $info) {
$form[$id . '_info']['#markup'] .= '<strong>' . $field . '</strong>' . ': ' . $info . '<br />';
}
$form[$id . '_file'] = [
'#type' => 'file',
'#title' => $def['label'],
'#weight' => 5,
'#states' => [
'visible' => [
':input[name="import_type"]' => ['value' => "$id"],
':input[name="format"]' => ['value' => "file"],
],
],
];
$form[$id . '_paste'] = [
'#title' => $this->t('Paste your csv file here'),
'#type' => 'textarea',
'#placeholder' => '"'.implode('","', $col_names).'"',
'#weight' => 6,
'#states' => [
'visible' => [
':input[name="import_type"]' => ['value' => "$id"],
':input[name="format"]' => ['value' => "paste"],
],
],
];
$form[$id . '_url'] = [
'#title' => $this->t('Url of csv file'),
'#placeholder' => 'http://',
'#type' => 'textfield',
'#weight' => 6,
'#states' => [
'visible' => [
':input[name="import_type"]' => ['value' => "$id"],
':input[name="format"]' => ['value' => "url"],
],
],
];
}
else {
$form['import_type'][$id]['#disabled'] = TRUE;
}
}
$form['delete'] = array(
'#title' => t('Delete existing'),
'#type' => 'checkbox',
'#default_value' => FALSE,
'#weight' => 9,
);
$form['save'] = array(
'#title' => t('Save entities after validation'),
'#type' => 'checkbox',
'#default_value' => FALSE,
'#weight' => 9,
);
$form['submit'] = array(
'#type' => 'submit',
'#value' => 'import',
'#weight' => 10,
);
return $form;
}
/**
* {@inheritDoc}
*/
public function validateForm(array &$form, FormStateInterface $form_state) {
if ($form_state->getValue('format') == 'file') {
$element_name = $form_state->getValue('import_type') . '_file';
// Check for a new uploaded file.
$file = file_save_upload($element_name, ['file_validate_extensions' => ['csv']], FALSE, 0);
if (isset($file)) {
// File upload was attempted.
if ($file) {
// Put the temporary file in form_values so we can save it on submit.
$form_state->setValue($element_name, $file);
}
else {
// File upload failed.
$form_state->setErrorByName($element_name, $this->t('The csv could not be uploaded.'));
}
}
}
$type = $form_state->getValue('import_type');
$this->setPlugin($type);
if ($form_state->getValue('format') == 'file') {
$file_entity = $form_state->getValue($type . '_file');
$csv_str = file_get_contents($file_entity->getFileUri());
}
elseif ($form_state->getValue('format') == 'url') {
$url = $form_state->getValue($type . '_url');
$csv_str = \Drupal::httpClient()->get($url)->getBody()->getContents();
}
else {
$csv_str = $form_state->getValue($type . '_paste');
}
$form_state->set('csv', $this->parseCsv($csv_str));
}
/**
* Load the given plugin.
*/
public function setPlugin(string $id) {
$this->plugin = $this->csvImporter->createInstance($id);
}
/**
* get only the deepest subclass of plugins
*/
private function getImportPluginDefs() {
$all_defs = $this->csvImporter->getDefinitions();
$defs = $all_defs;
foreach ($all_defs as $def1) {
foreach ($defs as $d2 => $def2) {
if (is_subclass_of($def1['class'], $def2['class'])) {
unset($defs[$d2]);
break;
}
}
}
return $defs;
}
/**
* {@inheritDoc}
*/
public function submitForm(array &$form, FormStateInterface $form_state) {
// Remember the form settings.
// Tidy up.
if ($form_state->getValue('format') == 'file') {
$form_state->getValue($form_state->getValue('import_type') . '_file')->delete();
}
$_SESSION['import']['format'] = $form_state->getValue('format');
$rows = $this->plugin->getCsvRows($form_state->get('csv'));
if (!$rows) {
\Drupal::messenger()->addError('Csv not parsed');
}
if (count($rows) > 100) {
$batch = $this->plugin->makeBatch($rows, $form_state->getValue('delete'), $form_state->getValue('save'));
batch_set($batch);
}
else {
if ($form_state->getValue('delete')) {
$this->plugin->deleteAll();
}
$this->plugin::saveEntities($this->plugin->getPluginId(), $rows, $form_state->getValue('save'));
}
}
function parseCsv($str) {
$str = str_replace('", "', '","', $str); // remove spaces between fields
$str = str_replace(',,', ',"",', $str); // quote unquoted empty fields
// Match all the non-quoted text and one series of quoted text (or the end of the string)
// Each group of matches will be parsed with the callback, with $matches[1] containing all the non-quoted text,
// nd $matches[3] containing everything inside the quotes
$str = preg_replace_callback('/([^"]*)("((""|[^"])*)"|$)/s', [$this, 'parseCsvQuotes'], $str);
//remove the very last newline to prevent a 0-field array for the last line
$str = preg_replace('/\n$/', '', $str);
//split on LF and parse each line with a callback
return array_map([$this, 'parseCsvLine'], explode("\n", $str));
}
//replace all the csv-special characters inside double quotes with markers using an escape sequence
function parseCsvQuotes(array $matches) {
if (!isset($matches[3]))return $matches[1];
//anything inside the quotes that might be used to split the string into lines and fields later,
//needs to be quoted. The only character we can guarantee as safe to use, because it will never appear in the unquoted text, is a CR
//So we're going to use CR as a marker to make escape sequences for CR, LF, Quotes, and Commas.
$str = str_replace("\r", "\rR", $matches[3]);
$str = str_replace("\n", "\rN", $str);
$str = str_replace('""', "\rQ", $str);
$str = str_replace(',', "\rC", $str);
//The unquoted text is where commas and newlines are allowed, and where the splits will happen
//We're going to remove all CRs from the unquoted text, by normalizing all line endings to just LF
//This ensures us that the only place CR is used, is as the escape sequences for quoted text
return preg_replace('/\r\n?/', "\n", $matches[1]) . $str;
}
//split on comma and parse each field with a callback
function parseCsvLine($line) {
return array_map([$this, 'parseCsvField'], explode(',', trim($line)));
}
//restore any csv-special characters that are part of the data
function parseCsvField($field) {
$field = str_replace("\rC", ',', $field);
$field = str_replace("\rQ", '"', $field);
$field = str_replace("\rN", "\n", $field);
$field = str_replace("\rR", "\r", $field);
return $field;
}
}
