This is actually my first technical post since I've started the blog as I've been using it more for writing other kind of stuff.
Here goes.
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');
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');
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();
}
}
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
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
We then define a variable for showing or hiding one of the columns:
$show_company_column=true;
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');
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']);
}
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
Credits
Thanks to https://www.generatedata.com/ for generating the demo data, and to https://highlightjs.org/ for the code blocks highlight.