Import Excel rows into a Drupal content type using batch process

We are creating a  module that lets a site admin upload an Excel (.xlsx) file and import each row as a Drupal node (a content type) using Drupal Batch API. Batch processing keeps the UI responsive and safe for large uploads. The example uses PhpSpreadsheet to parse Excel files.


Prerequisites

  1. Drupal 9/10/11 site.
  2. Composer support in your project.
  3. Install PhpSpreadsheet:

composer require phpoffice/phpspreadsheet

      4. Create a custom module folder, e.g. modules/custom/dn_import.


Assume we have content type sample_student with fields Card number, student name, class name  and bus route. Using this excel we are going to insert data into this content type,

Files needed 

dn_import/

  dn_import.info.yml

  dn_import.routing.yml

  src/Form/ExcelImportForm.php

dn_import.info.yml

name: dn_import
type: module
description: dn_import module created by DigitalNadeem.com.
package: Custom
version: 1.0
core_version_requirement: ^9 || ^10 || ^11

dn_import.routing.yml (admin page)

dn_import.import_form:
 path: '/admin/content/student-import-sheet'
 defaults:
   _form: '\Drupal\dn_import\Form\StudentImportForm'
   _title: 'Import Students'
 requirements:
   _permission: 'administer nodes'

Create a form with a file upload field in form.

On submit:

  • Read the Excel file with PhpSpreadsheet.
  • Extract header row (column names) & data rows.
  • Optionally validate column names and row counts.
  • Build batch operations (process rows in chunks).
  • Start Batch API.
  • Batch callback: create nodes from rows, track created/fail counts.
  • Batch finished callback: show summary message.

Create a form  – /src/Form/StudentImportForm.php

In this form we have a file upload field and link to a sample excel template. This template is used to fill the data and upload in the same form.

At the top of form calls file include php spreadsheet –  use PhpOffice\PhpSpreadsheet\IOFactory;

Create form build function as below 

public function buildForm(array $form, FormStateInterface $form_state) {
       // Provide a download link for the Excel template.
     $form['template_download'] = [
     '#markup' => $this->t(
       '<a href="/web/sites/default/files/students-sheet.xlsx" download>Download Excel Template</a>'
     ),
   ];




   $form['excel_file'] = [
     '#type' => 'file',
     '#title' => $this->t('Upload Excel File (.xlsx)'),
     '#required' => TRUE,
   ];
   $form['submit'] = [
     '#type' => 'submit',
     '#value' => $this->t('Import Students'),
   ];
   return $form;
 }

This is the sample excel we are going to import.

First create submit form function.

public function submitForm(array &$form, FormStateInterface $form_state) {
}

Inside submit form, read the excel rows and save it in $rows variable.

if ($file) {
     $filepath = $file->getFileUri();
     $realpath = \Drupal::service('file_system')->realpath($filepath);


     $spreadsheet = IOFactory::load($realpath);
     $sheet = $spreadsheet->getActiveSheet();
     $rows = [];
     foreach ($sheet->toArray() as $index => $row) {
       if ($index == 0) continue; // skip header
       $rows[] = $row;
     }

Inside  above if condition,  after loading all rows data in $rows variable, set the batch operation.

$batch = [
       'title' => $this->t('Importing Students...'),
       'operations' => [],
       'finished' => [self::class, 'batchFinished'],
     ];


     foreach ($rows as $i => $row) {
       $batch['operations'][] = [
         [get_class($this), 'processRow'], [$row, $i]
       ];
     }


     batch_set($batch);

Here processRow callback function processes each row, Drupal’s batch API executes processRow to process rows in batches.

‘batchFinished’ is the call back where we are showing a summary of the batch execution.

Implement processRows as below.

public static function processRow($row, $i, array &$context) {
       $card_number = trim($row[0]);
       $student_name = trim($row[1]);
       $class_value = trim($row[2]);
       $bus_route_name = trim($row[3]);


       // Check for duplicate card number (title)
       $existing = \Drupal::entityTypeManager()->getStorage('node')
           ->loadByProperties([
           'type' => 'student',
           'title' => $card_number,
           ]);


       if (!empty($existing)) {
           $context['results']['duplicates'][] = [
           'student_name' => $student_name,
           'card_number' => $card_number,
           ];
           return; // skip
       }


     


       $node = \Drupal\node\Entity\Node::create([
           'type' => 'student',
           'title' => $card_number,
           'field_student_name' => $student_name,
           'field_class' => $class_value,
           'field_bus_point' => $bus_route_name,
       ]);
       $node->save();


       // update counter
       if (!isset($context['results']['imported_count'])) {
           $context['results']['imported_count'] = 0;
       }
       $context['results']['imported_count']++;


       $context['message'] = t('Processed @name', ['@name' => $student_name]);
}

Here “$context” variable is very important as it will update the status of batches to the batch API. for each row we are creating a node using Node::create() function.

After creation of nodes , we update $context array. Here $context[‘results’][‘imported_count’] this the batch counter and messages are stored in $context[‘message’]. Later we will retrieve this message variable in batchFinished call back function. 

Next we have implement batchFinished call back as below.

public static function batchFinished($success, $results, $operations) {
   if (isset($results['imported_count'])) {
     \Drupal::messenger()->addStatus(t('Total students imported: @count', [
       '@count' => $results['imported_count'],
     ]));
   }


   if (!empty($results['duplicates'])) {
     $list = '<ul>';
     foreach ($results['duplicates'] as $dup) {
       $list .= '<li>' . $dup['student_name'] . ' (Card Number: ' . $dup['card_number'] . ')</li>';
     }
     $list .= '</ul>';
     \Drupal::messenger()->addWarning(t('Skipped duplicate card numbers: @list', ['@list' => $list]));
   }
}

After completion of all messages this function will execute.

Access the page using path – /admin/content/student-import-sheet

The page looks like as below.

Complete source code you can download here.

Get Free E-book
Get a free Ebook on Drupal 8 -theme tutorial
I agree to have my personal information transfered to MailChimp ( more information )

You may also like...