DEV Community

Cover image for Generating Excel sheet with dynamic column names
Alexandru Trandafir
Alexandru Trandafir

Posted on

Generating Excel sheet with dynamic column names

The problem

When we export an Excel sheet from PHP, or other language, the most common use case is to export a table from the database, and some of the times it also contains some information in the header, and a footer with some totals.

Bottom line, we have a table, and some columns.

This is how you'd go normally, by placing manually each column letter in the different parts of the document, such as:

  • Header
  • Rows
  • Styling
$spreadsheet = new Spreadsheet();
$sheet = $spreadsheet->getActiveSheet();
$sheet->setCellValue('A1', 'Normal way of naming Excel columns');

$i=3;

// Header
$sheet->setCellValue('A'.$i, 'Name');
$sheet->setCellValue('B'.$i, 'Phone');
$sheet->setCellValue('C'.$i, 'City');

// Rows
foreach ($rows as $row) {
  $i++;
  $sheet->setCellValue('A'.$i, $row['name']);
  $sheet->setCellValue('B'.$i, $row['phone']);
  $sheet->setCellValue('C'.$i, $row['city']);
}

// Styling
$lastRowIndex=$i;

$sheet->getStyle('A3:C'.$lastRowIndex)->applyFromArray([
    'borders' => [
        'allBorders' => [
            'borderStyle' => \PhpOffice\PhpSpreadsheet\Style\Border::BORDER_THIN,
        ],
    ],
]);

$sheet->getStyle('A3:C3')->getFill()
    ->setFillType(\PhpOffice\PhpSpreadsheet\Style\Fill::FILL_SOLID)
    ->getStartColor()->setARGB('FFA0A0A0');

$sheet->getColumnDimension('A')->setWidth(12);
$sheet->getColumnDimension('B')->setWidth(25);
$sheet->getColumnDimension('C')->setWidth(25);

$writer = new Xlsx($spreadsheet);
$writer->save('normal.xlsx');
Enter fullscreen mode Exit fullscreen mode

The problem with this approach is that if you want to add new columns, you'll have to spend some time renaming all the letters. And you'll also have to remember which letter you have assigned to each column when you apply styling.

So if you want to add the column Company after Phone, you'll have to put it on the letter C, and rename the next column C to D, and do this in renaming everywhere, not just in the header, also in the rows and in the styling, etc.

You'd say it's a quick task, but..

  • What if you had 10-20 columns or more?
  • What if you wanted to have some cases when you show a column and other cases when you want to hide it based on user preference when exporting?

The solution

Okay it might not be the best solution, I'm sure others might think of better ways to do it but this is one way to avoid spending lots of time renaming the columns.

// Load the column names helper
require 'ExCol.php';

ExCol::reset(); // reset mapping before using

$show_company_column=true;

$spreadsheet = new Spreadsheet();
$sheet = $spreadsheet->getActiveSheet();
$sheet->setCellValue('A1', 'Dynamic way of naming Excel columns');

$i=3;

// Header
$sheet->setCellValue(ExCol::get('name', $i), 'Name');
$sheet->setCellValue(ExCol::get('phone', $i), 'Phone');
if ($show_company_column) {
  $sheet->setCellValue(ExCol::get('company', $i), 'Company');
}
$sheet->setCellValue(ExCol::get('city', $i), 'City');

// Rows
foreach ($rows as $row) {
  $i++;
  $sheet->setCellValue(ExCol::get('name', $i), $row['name']);
  $sheet->setCellValue(ExCol::get('phone', $i), $row['phone']);
  if ($show_company_column) {
    $sheet->setCellValue(ExCol::get('company', $i), $row['company']);
  }
  $sheet->setCellValue(ExCol::get('city', $i), $row['city']);
}

$lastColLetter=ExCol::getLast();

// Styling
$lastRowIndex=$i;

$sheet->getStyle('A3:'.$lastColLetter.$lastRowIndex)->applyFromArray([
    'borders' => [
        'allBorders' => [
            'borderStyle' => \PhpOffice\PhpSpreadsheet\Style\Border::BORDER_THIN,
        ],
    ],
]);

$sheet->getStyle("A3:{$lastColLetter}3")->getFill()
    ->setFillType(\PhpOffice\PhpSpreadsheet\Style\Fill::FILL_SOLID)
    ->getStartColor()->setARGB('FFA0A0A0');

$sheet->getColumnDimension(ExCol::get('name'))->setWidth(12);
if ($show_company_column) {
  $sheet->getColumnDimension(ExCol::get('company'))->setWidth(35);
}
$sheet->getColumnDimension(ExCol::get('phone'))->setWidth(25);
$sheet->getColumnDimension(ExCol::get('city'))->setWidth(25);

$writer = new Xlsx($spreadsheet);
$writer->save('dynamic.xlsx');
Enter fullscreen mode Exit fullscreen mode

So how does it work?

Well, we have created a small helper class named ExCol.

I know, I could have found a better name but at least it is short.

<?php

use PhpOffice\PhpSpreadsheet\Cell\Coordinate;

class ExCol {

  static private $_map=array();

  static public function get($col, $row=null) {
    if (!in_array($col, self::$_map)) {
      self::$_map[]=$col;
    }
    $index = array_search($col, self::$_map);
    $columnLetter = Coordinate::stringFromColumnIndex($index + 1);
    return $columnLetter.($row?$row:null);
  }

  static public function getLast() {
    return Coordinate::stringFromColumnIndex(count(self::$_map));
  }

  static public function reset() {
    self::$_map=array();
  }

}
Enter fullscreen mode Exit fullscreen mode

With this class, every time we need a letter for a column, we just call the method ExCol::get().

Examples:

ExCol::get('my_first_column'); // Returns A
ExCol::get('my_second_column'); // Returns B
ExCol::get('my_third_column'); // Returns C
ExCol::get('my_third_column', 2); // Returns C2
Enter fullscreen mode Exit fullscreen mode

Optionally we can also pass a $row number so instead of getting back C, we'll get C2 and avoid having to use concatenation.

So the class stores statically an index of the columns requested in the order they were requested. And based on the index assigned to each column, it will return its letter, by using PhpSpreadsheet's Coordinate::stringFromColumnIndex() method.

So let's go over the new parts in the code.

If we use the helper on a script that is used to generate multiple Excel files, we might want to call the reset() method to clear the index just in case so it won't contain the columns of the previous file.

ExCol::reset(); // reset mapping before using
Enter fullscreen mode Exit fullscreen mode

We then define a variable for showing or hiding one of the columns:

$show_company_column=true;
Enter fullscreen mode Exit fullscreen mode

And when printing the header, we only include that column if required:

// Header
$sheet->setCellValue(ExCol::get('name', $i), 'Name');
$sheet->setCellValue(ExCol::get('phone', $i), 'Phone');
if ($show_company_column) {
  $sheet->setCellValue(ExCol::get('company', $i), 'Company');
}
$sheet->setCellValue(ExCol::get('city', $i), 'City');
Enter fullscreen mode Exit fullscreen mode

Same happens when looping the rows and with the styling:

// Rows
foreach ($rows as $row) {
  $i++;
  $sheet->setCellValue(ExCol::get('name', $i), $row['name']);
  $sheet->setCellValue(ExCol::get('phone', $i), $row['phone']);
  if ($show_company_column) {
    $sheet->setCellValue(ExCol::get('company', $i), $row['company']);
  }
  $sheet->setCellValue(ExCol::get('city', $i), $row['city']);
}
Enter fullscreen mode Exit fullscreen mode

So you can see we're dynamically including one column, so if we only put 3 columns we'll the helper will give us A, B and C, but if we put 4, we'll get A, B, C, D.

If you want to control dynamically the columns to show, or you need to add more columns or remove some, there will be no need for renaming anything as it will all happen automatically.

Full source code here:

https://github.com/atrandafir/excel-dynamic-columns

Original post at:

https://atrandafir.dev/post/generating-excel-sheet-dynamic-column-names

Discussion (0)