Man working on computer

Import Drupal 8 Content From a CSV File

By Dave

Whether you are launching a brand new Drupal 8 site, or you simply have new content that you want to import without too much repetition, it can help to build an import tool. In this tutorial, I will show a technique of uploading a CSV file and creating content in a batch process. This post will show the basics of creating a module to import Drupal 8 content from a CSV file.

This tutorial assumes that you already know about creating a basic Drupal 8 module with routes, controllers, etc.

Some of the code includes some leftover from the original script that was written for a specific client, so it may be able to be cleaned up for your use.

Let's start with a simple CSV File. However you create this is up to and with this script, you'll be able to see how you can add more fields(columns) as well as do some processing on each item.

(since a CSV is quite literal, make sure that if there are commas within content, you account for that in your script otherwise it will break on import and nothing will make sense. A simple google search will help, or this link may help.)

title,content,id
How to feed a cat,Here are the instructions on how to feed a cat,1
How to feed a dog,Here are the instructions on how to feed a dog,2
How to feed a bird, Here are the instructions on how to feed a bird,3

First, you should create a custom page with a form that allows you to upload the file so it can be processed.

In your IMPORT_EXAMPLE.routing.yml file

IMPORT_EXAMPLE.content:
  path: '/import-example'
  defaults:
    _controller: '\Drupal\IMPORT_EXAMPLE\Controller\ImportPage::content'
    _title: 'EXAMPLE Data Import'
  requirements:
    _permission: 'access content'

In the /src/Controller/ImportPage.php file you are telling Drupal to put a custom form that we will build in the next step.

<?php

namespace Drupal\IMPORT_EXAMPLE\Controller;
use Drupal\Core\Controller\ControllerBase;
use Symfony\Component\HttpFoundation\Request;
use Drupal\Core\Access\AccessResult; 
use Drupal\Core\Form\FormInterface;

class ImportPage extends ControllerBase {
  /**
   * Display the markup.
   *
   * @return array
   */
  public function content(Request $request) {

    $form = \Drupal::formBuilder()->getForm('Drupal\IMPORT_EXAMPLE\Form\ImportForm');
    
    return $form;
  }
}

In the /src/Form/ImportForm.php file you will create the form with a file upload feature that will then send the data to a batch queue. The $operations variable and batch_set() function will work with the data that has been extracted from the file. I'm including a function in this file to convert the CSV to an array. You can plug in any function you desire if you need to manipulate the data any differently.

<?php
/**
 * @file
 * Contains \Drupal\IMPORT_EXAMPLE\Form\ImportForm.
 */
namespace Drupal\IMPORT_EXAMPLE\Form;
use Drupal\Core\Form\FormBase;
use Drupal\Core\Form\FormStateInterface;
use Drupal\node\Entity\Node;
use Drupal\file\Entity\File;

class ImportForm extends FormBase {
  /**
   * {@inheritdoc}
   */
  public function getFormId() {
    return 'example_import_form';
  }
  /**
   * {@inheritdoc}
   */
  public function buildForm(array $form, FormStateInterface $form_state) {

    $form['description'] = array(
      '#markup' => '<p>Use this form to upload a CSV file of Data</p>',
    );

    $form['import_csv'] = array(
      '#type' => 'managed_file',
      '#title' => t('Upload file here'),
      '#upload_location' => 'public://importcsv/',
      '#default_value' => '',
      "#upload_validators"  => array("file_validate_extensions" => array("csv")),
      '#states' => array(
        'visible' => array(
          ':input[name="File_type"]' => array('value' => t('Upload Your File')),
        ),
      ),
    );

    $form['actions']['#type'] = 'actions';


    $form['actions']['submit'] = array(
      '#type' => 'submit',
      '#value' => $this->t('Upload CSV'),
      '#button_type' => 'primary',
    );

    return $form;
  }

  /**
   * {@inheritdoc}
   */
  public function submitForm(array &$form, FormStateInterface $form_state) {


    /* Fetch the array of the file stored temporarily in database */
    $csv_file = $form_state->getValue('import_csv');

    /* Load the object of the file by it's fid */
    $file = File::load( $csv_file[0] );

    /* Set the status flag permanent of the file object */
    $file->setPermanent();

    /* Save the file in database */
    $file->save();

    // You can use any sort of function to process your data. The goal is to get each 'row' of data into an array
    // If you need to work on how data is extracted, process it here.
    $data = $this->csvtoarray($file->getFileUri(), ',');
    foreach($data as $row) {
      $operations[] = ['\Drupal\IMPORT_EXAMPLE\addImportContent::addImportContentItem', [$row]];
    }

    $batch = array(
      'title' => t('Importing Data...'),
      'operations' => $operations,
      'init_message' => t('Import is starting.'),
      'finished' => '\Drupal\IMPORT_EXAMPLE\addImportContent::addImportContentItemCallback',
    );
    batch_set($batch);
  }

  public function csvtoarray($filename='', $delimiter){

    if(!file_exists($filename) || !is_readable($filename)) return FALSE;
    $header = NULL;
    $data = array();

    if (($handle = fopen($filename, 'r')) !== FALSE ) {
      while (($row = fgetcsv($handle, 1000, $delimiter)) !== FALSE)
      {
        if(!$header){
          $header = $row;
        }else{
          $data[] = array_combine($header, $row);
        }
      }
      fclose($handle);
    }

    return $data;
  }

}

In the /src/addImportContent.php file you will actually process each item using the Batch API system within Drupal.

<?php
namespace Drupal\IMPORT_EXAMPLE;

use Drupal\node\Entity\Node;

class addImportContent {
  public static function addImportContentItem($item, &$context){
    $context['sandbox']['current_item'] = $item;
    $message = 'Creating ' . $item['title'];
    $results = array();
    create_node($item);
    $context['message'] = $message;
    $context['results'][] = $item;
  }
  function addImportContentItemCallback($success, $results, $operations) {
    // The 'success' parameter means no fatal PHP errors were detected. All
    // other error management should be handled using 'results'.
    if ($success) {
      $message = \Drupal::translation()->formatPlural(
        count($results),
        'One item processed.', '@count items processed.'
      );
    }
    else {
      $message = t('Finished with an error.');
    }
    drupal_set_message($message);
  }
}

// This function actually creates each item as a node as type 'Page'
function create_node($item) {
  $node_data['type'] = 'page';
  $node_data['title'] = $item['title'];
  $node_data['body']['value'] = $item['content'];
  // Setting a simple textfield to add a unique ID so we can use it to query against if we want to manipulate this data again.
  $node_data['field_unique_id']['value'] = $item['id'];
  $node = Node::create($node_data);
  $node->setPublished(TRUE);
  $node->save();
} 

Assuming there are no conflicts (errors or bugs as one might say) in the code, you'll simply clear your cache, navigate to /import-example, upload a file and watch the magic happen!

When testing these scripts, it's best to have a CSV with only a handful of rows so you can test, delete, rinse, repeat.