diff --git a/README.md b/README.md index cbe1a78..0abeb5d 100644 --- a/README.md +++ b/README.md @@ -63,3 +63,10 @@ For more details and examples see ## Maintainers Lasha Badashvili - [lashabp](https://www.drupal.org/u/lashabp) +CSV importer module helps to import content from CSV files. +

Command-line usage:

+ + diff --git a/src/Form/ImporterForm.php b/src/Form/ImporterForm.php index eeb0a80..cc14599 100644 --- a/src/Form/ImporterForm.php +++ b/src/Form/ImporterForm.php @@ -2,11 +2,11 @@ namespace Drupal\csv_importer\Form; +use Drupal\Core\Entity\EntityFieldManagerInterface; +use Drupal\Core\Entity\EntityTypeBundleInfoInterface; +use Drupal\Core\Entity\EntityTypeManagerInterface; use Drupal\Core\Form\FormBase; use Drupal\Core\Form\FormStateInterface; -use Drupal\Core\Entity\EntityTypeManagerInterface; -use Drupal\Core\Entity\EntityTypeBundleInfoInterface; -use Drupal\Core\Entity\EntityFieldManagerInterface; use Drupal\Core\Render\RendererInterface; use Drupal\csv_importer\ParserInterface; use Drupal\csv_importer\Plugin\ImporterManager; @@ -271,102 +271,24 @@ class ImporterForm extends FormBase { return $options; } - /** - * Get entity type fields. - * - * @param string $entity_type - * Entity type. - * @param string|null $entity_type_bundle - * Entity type bundle. - * - * @return array - * Entity type fields. - */ - protected function getEntityTypeFields(string $entity_type, string $entity_type_bundle = NULL) { - $fields = []; - - if (!$entity_type_bundle) { - $entity_type_bundle = key($this->entityBundleInfo->getBundleInfo($entity_type)); - } - - $entity_fields = $this->entityFieldManager->getFieldDefinitions($entity_type, $entity_type_bundle); - foreach ($entity_fields as $entity_field) { - $fields['fields'][] = $entity_field->getName(); - - if ($entity_field->isRequired()) { - $fields['required'][] = $entity_field->getName(); - } - } - - return $fields; - } - - /** - * Get entity missing fields. - * - * @param string $entity_type - * Entity type. - * @param array $required - * Entity required fields. - * @param array $csv - * Parsed CSV. - * - * @return array - * Missing fields. - */ - protected function getEntityTypeMissingFields(string $entity_type, array $required, array $csv) { - $entity_definition = $this->entityTypeManager->getDefinition($entity_type); - - if ($entity_definition->hasKey('bundle')) { - unset($required[array_search($entity_definition->getKey('bundle'), $required)]); - } - - $csv_fields = []; - - if (!empty($csv)) { - foreach ($csv[0] as $csv_row) { - $csv_row = explode('|', $csv_row); - $csv_fields[] = $csv_row[0]; - } - } - - $csv_fields = array_values(array_unique($csv_fields)); - - return array_diff($required, $csv_fields); - } - /** * {@inheritdoc} */ public function submitForm(array &$form, FormStateInterface $form_state) { $entity_type = $form_state->getValue('entity_type'); $entity_type_bundle = NULL; - $csv = current($form_state->getValue('csv')); - $csv_parse = $this->parser->getCsvById($csv, $form_state->getUserInput()['delimiter']); if (isset($form_state->getUserInput()['entity_type_bundle'])) { $entity_type_bundle = $form_state->getUserInput()['entity_type_bundle']; } + $csv_file_id = current($form_state->getValue('csv')); + $delimiter = $form_state->getUserInput()['delimiter']; + $plugin_id = $form_state->getUserInput()['plugin_id']; - $entity_fields = $this->getEntityTypeFields($entity_type, $entity_type_bundle); - - if ($required = $this->getEntityTypeMissingFields($entity_type, $entity_fields['required'], $csv_parse)) { - $render = [ - '#theme' => 'item_list', - '#items' => $required, - ]; + /** @var \Drupal\file\Entity\File $entity */ + $csv_entity = $this->parser->getCsvEntity($csv_file_id); - $this->messenger()->addError($this->t('Your CSV has missing required fields: @fields', ['@fields' => $this->renderer->render($render)])); - } - else { - $this->importer->createInstance($form_state->getUserInput()['plugin_id'], [ - 'csv' => $csv_parse, - 'csv_entity' => $this->parser->getCsvEntity($csv), - 'entity_type' => $entity_type, - 'entity_type_bundle' => $entity_type_bundle, - 'fields' => $entity_fields['fields'], - ])->process(); - } + $this->importer->import($entity_type, $entity_type_bundle, $csv_entity->uri->getString(), $delimiter, $plugin_id, TRUE); } } diff --git a/src/Parser.php b/src/Parser.php index f242854..36b131a 100644 --- a/src/Parser.php +++ b/src/Parser.php @@ -26,38 +26,6 @@ class Parser implements ParserInterface { $this->entityTypeManager = $entity_type_manager; } - /** - * {@inheritdoc} - */ - public function getCsvById(int $id, string $delimiter) { - /** @var \Drupal\file\Entity\File $entity */ - $entity = $this->getCsvEntity($id); - $return = []; - - if (($csv = fopen($entity->uri->getString(), 'r')) !== FALSE) { - while (($row = fgetcsv($csv, 0, $delimiter)) !== FALSE) { - $return[] = $row; - } - - fclose($csv); - } - - return $return; - } - - /** - * {@inheritdoc} - */ - public function getCsvFieldsById(int $id) { - $csv = $this->getCsvById($id); - - if ($csv && is_array($csv)) { - return $csv[0]; - } - - return NULL; - } - /** * {@inheritdoc} */ diff --git a/src/ParserInterface.php b/src/ParserInterface.php index 7db97e3..f50478e 100644 --- a/src/ParserInterface.php +++ b/src/ParserInterface.php @@ -7,30 +7,6 @@ namespace Drupal\csv_importer; */ interface ParserInterface { - /** - * Get CSV by id. - * - * @param int $id - * CSV id. - * @param string $delimiter - * CSV delimiter. - * - * @return array|null - * Parsed CSV. - */ - public function getCsvById(int $id, string $delimiter); - - /** - * Get CSV column (first row). - * - * @param int $id - * CSV id. - * - * @return array|null - * CSV field names. - */ - public function getCsvFieldsById(int $id); - /** * Load CSV. * diff --git a/src/Plugin/ImporterBase.php b/src/Plugin/ImporterBase.php index 37f7ffe..1c3d713 100644 --- a/src/Plugin/ImporterBase.php +++ b/src/Plugin/ImporterBase.php @@ -2,15 +2,15 @@ namespace Drupal\csv_importer\Plugin; -use Drupal\Core\Plugin\PluginBase; -use Drupal\Core\Entity\EntityTypeManagerInterface; +use Drupal\Component\Utility\Unicode; use Drupal\Core\Config\ConfigFactoryInterface; -use Drupal\Core\StringTranslation\StringTranslationTrait; -use Drupal\Core\Logger\LoggerChannelFactoryInterface; +use Drupal\Core\Entity\EntityTypeManagerInterface; use Drupal\Core\Extension\ModuleHandlerInterface; use Drupal\Core\File\FileSystemInterface; +use Drupal\Core\Logger\LoggerChannelFactoryInterface; +use Drupal\Core\Plugin\PluginBase; +use Drupal\Core\StringTranslation\StringTranslationTrait; use Drupal\file\FileRepositoryInterface; -use Drupal\Component\Utility\Unicode; use Symfony\Component\DependencyInjection\ContainerInterface; /** @@ -256,4 +256,17 @@ abstract class ImporterBase extends PluginBase implements ImporterInterface { batch_set($process); } + /** + * {@inheritdoc} + */ + public function processNow() { + $context = []; + if ($data = $this->data()) { + foreach ($data['content'] as $content) { + $this->add($content, $context); + } + } + $this->finished(TRUE, $context, []); + } + } diff --git a/src/Plugin/ImporterInterface.php b/src/Plugin/ImporterInterface.php index ec0daa0..a207ba8 100644 --- a/src/Plugin/ImporterInterface.php +++ b/src/Plugin/ImporterInterface.php @@ -51,4 +51,9 @@ interface ImporterInterface extends PluginInspectionInterface, ContainerFactoryP */ public function process(); + /** + * Process without running a batch operation. + */ + public function processNow(); + } diff --git a/src/Plugin/ImporterManager.php b/src/Plugin/ImporterManager.php index 68c0519..d4efef0 100644 --- a/src/Plugin/ImporterManager.php +++ b/src/Plugin/ImporterManager.php @@ -4,12 +4,16 @@ namespace Drupal\csv_importer\Plugin; use Drupal\Core\Cache\CacheBackendInterface; use Drupal\Core\Extension\ModuleHandlerInterface; +use Drupal\Core\Messenger\MessengerTrait; use Drupal\Core\Plugin\DefaultPluginManager; +use Drupal\Core\StringTranslation\StringTranslationTrait; /** * Provides the Importer plugin manager. */ class ImporterManager extends DefaultPluginManager { + use MessengerTrait; + use StringTranslationTrait; /** * Constructs a ImporterManager object. @@ -29,4 +33,180 @@ class ImporterManager extends DefaultPluginManager { $this->setCacheBackend($cache_backend, 'importer_info_plugins'); } + /** + * Mockable wrapper around the entity_type.bundle.info service. + */ + protected function entityBundleInfo() { + return \Drupal::service('entity_type.bundle.info'); + } + + /** + * Mockable wrapper around the entity_type.manager service. + */ + protected function entityTypeManager() { + return \Drupal::service('entity_type.manager'); + } + + /** + * Mockable wrapper around the entity_field.manager service. + */ + protected function entityFieldManager() { + return \Drupal::service('entity_field.manager'); + } + + /** + * Mockable wrapper around the renderer service. + */ + protected function renderer() { + return \Drupal::service('renderer'); + } + + /** + * Get entity missing fields. + * + * @param string $entity_type + * Entity type. + * @param array $required + * Entity required fields. + * @param array $csv + * Parsed CSV. + * + * @return array + * Missing fields. + */ + protected function getEntityTypeMissingFields(string $entity_type, array $required, array $csv) { + $entity_definition = $this->entityTypeManager()->getDefinition($entity_type); + + if ($entity_definition->hasKey('bundle')) { + unset($required[array_search($entity_definition->getKey('bundle'), $required)]); + } + + $csv_fields = []; + + if (!empty($csv)) { + foreach ($csv[0] as $csv_row) { + $csv_row = explode('|', $csv_row); + $csv_fields[] = $csv_row[0]; + } + } + + $csv_fields = array_values(array_unique($csv_fields)); + + return array_diff($required, $csv_fields); + } + + /** + * Get entity type fields. + * + * @param string $entity_type + * Entity type. + * @param string|null $entity_type_bundle + * Entity type bundle. + * + * @return array + * Entity type fields. + */ + protected function getEntityTypeFields(string $entity_type, string $entity_type_bundle = NULL) { + $fields = []; + + if (!$entity_type_bundle) { + $entity_type_bundle = key($this->entityBundleInfo()->getBundleInfo($entity_type)); + } + + $entity_fields = $this->entityFieldManager()->getFieldDefinitions($entity_type, $entity_type_bundle); + foreach ($entity_fields as $entity_field) { + $fields['fields'][] = $entity_field->getName(); + if ($entity_field->isRequired()) { + $fields['required'][] = $entity_field->getName(); + } + } + return $fields; + } + + /** + * Display an error due to missing fields. + * + * @param array $required + * The missing required fields. + * @param bool $batch + * Whether or not we are in the midst of a batch operation. + * + * @throws \Exception + */ + public function displayErrorMissingFields(array $required, bool $batch) { + if ($batch) { + $render = [ + '#theme' => 'item_list', + '#items' => $required, + ]; + + $this->messenger()->addError($this->t('Your CSV has missing required fields: @fields', ['@fields' => $this->renderer()->render($render)])); + } + else { + throw new \Exception('Your CSV has missing required fields: ' . implode(', ', $required)); + } + } + + /** + * {@inheritdoc} + */ + public function getCsvByFilePath(string $filepath, string $delimiter) { + $return = []; + + if (($csv = fopen($filepath, 'r')) !== FALSE) { + while (($row = fgetcsv($csv, 0, $delimiter)) !== FALSE) { + $return[] = $row; + } + + fclose($csv); + } + return $return; + } + + /** + * Import data from a CSV file. + * + * This is used by Drupal\csv_importer\Form\ImporterForm::submit(). You can + * also call this in code as follows: + * + * \Drupal::service('plugin.manager.importer')->import('node', 'article', + * '/full/path/to/my/csv/file.csv', ',', 'node_importer'); + * + * @param string $entity_type + * An entity type such as "node". + * @param mixed $entity_type_bundle + * An entity type bundle such as "article" or NULL. + * @param string $filepath + * A CSV filepath such as temporary://file.csv or /full/path/to/file.csv. + * @param string $delimiter + * A delimiter such as ','. + * @param string $plugin_id + * A plugin ID such as 'node_importer'. + * @param bool $batch + * Use TRUE if this is run from the GUI and FALSE if it's from from + * code. + */ + public function import(string $entity_type, $entity_type_bundle, string $filepath, string $delimiter, string $plugin_id, bool $batch = FALSE) { + $csv_parse = $this->getCsvByFilePath($filepath, $delimiter); + $entity_fields = $this->getEntityTypeFields($entity_type, $entity_type_bundle); + if ($required = $this->getEntityTypeMissingFields($entity_type, $entity_fields['required'], $csv_parse)) { + $this->displayErrorMissingFields($required, $batch); + } + else { + $importer = $this->createInstance($plugin_id, [ + 'csv' => $csv_parse, + 'csv_filepath' => $filepath, + 'entity_type' => $entity_type, + 'entity_type_bundle' => $entity_type_bundle, + 'fields' => $entity_fields['fields'], + ]); + if ($batch) { + $importer->process(); + } + else { + $importer->processNow(); + } + } + } + }