From 6bea337762b3b41d55000188a28de3bf10912312 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pavel=20Konvi=C4=8Dka?= Date: Wed, 11 Jun 2025 14:20:49 +0200 Subject: [PATCH 1/3] feat: Excel export --- composer.json | 3 +- src/Datagrid.php | 16 +++++- src/ExcelDataModel.php | 61 +++++++++++++++++++++ src/Export/ExportExcel.php | 49 +++++++++++++++++ src/Response/ExcelResponse.php | 97 ++++++++++++++++++++++++++++++++++ 5 files changed, 224 insertions(+), 2 deletions(-) create mode 100644 src/ExcelDataModel.php create mode 100644 src/Export/ExportExcel.php create mode 100644 src/Response/ExcelResponse.php diff --git a/composer.json b/composer.json index f7ca9550c..b10131d7f 100644 --- a/composer.json +++ b/composer.json @@ -29,7 +29,8 @@ "nette/di": "^3.0.0", "nette/forms": "^3.2.0", "nette/utils": "^4.0.0", - "symfony/property-access": "^6.4.0 || ^7.2.0" + "symfony/property-access": "^6.4.0 || ^7.2.0", + "phpoffice/phpspreadsheet": "^3.3" }, "require-dev": { "contributte/qa": "^0.3.0", diff --git a/src/Datagrid.php b/src/Datagrid.php index 4eff3ed26..a47adf9df 100644 --- a/src/Datagrid.php +++ b/src/Datagrid.php @@ -21,6 +21,7 @@ use Contributte\Datagrid\Exception\DatagridHasToBeAttachedToPresenterComponentException; use Contributte\Datagrid\Export\Export; use Contributte\Datagrid\Export\ExportCsv; +use Contributte\Datagrid\Export\ExportExcel; use Contributte\Datagrid\Filter\Filter; use Contributte\Datagrid\Filter\FilterDate; use Contributte\Datagrid\Filter\FilterDateRange; @@ -1581,6 +1582,19 @@ public function addExportCsv( return $exportCsv; } + public function addExportExcel( + string $text, + string $fileName, + bool $filtered = false + ): ExportExcel + { + $exportExcel = new ExportExcel($this, $text, $fileName, $filtered); + + $this->addToExports($exportExcel); + + return $exportExcel; + } + public function resetExportsLinks(): void { foreach ($this->exports as $id => $export) { @@ -1858,7 +1872,7 @@ public function handleExport(mixed $id): void $rows[] = new Row($this, $item, $this->getPrimaryKey()); } - if ($export instanceof ExportCsv) { + if ($export instanceof ExportCsv || $export instanceof ExportExcel) { $export->invoke($rows); } else { $export->invoke($items); diff --git a/src/ExcelDataModel.php b/src/ExcelDataModel.php new file mode 100644 index 000000000..93d41ac36 --- /dev/null +++ b/src/ExcelDataModel.php @@ -0,0 +1,61 @@ +getHeader(); + } + + foreach ($this->data as $item) { + $return[] = $this->getRow($item); + } + + return $return; + } + + public function getHeader(): array + { + $header = []; + + foreach ($this->columns as $column) { + $header[] = $this->translator->translate($column->getName()); + } + + return $header; + } + + /** + * Get item values saved into row + */ + public function getRow(mixed $item): array + { + $row = []; + + foreach ($this->columns as $column) { + $row[] = strip_tags((string) $column->render($item)); + } + + return $row; + } + +} diff --git a/src/Export/ExportExcel.php b/src/Export/ExportExcel.php new file mode 100644 index 000000000..aa4a96587 --- /dev/null +++ b/src/Export/ExportExcel.php @@ -0,0 +1,49 @@ +getExportCallback($name), + $filtered + ); + } + + private function getExportCallback(string $name): callable + { + return function ( + array $data, + DataGrid $grid + ) use ($name): void { + $columns = $this->getColumns(); + + if ($columns === []) { + $columns = $this->grid->getColumns(); + } + + $excelDataModel = new ExcelDataModel($data, $columns, $this->grid->getTranslator()); + + $this->grid->getPresenter()->sendResponse(new ExcelResponse($excelDataModel->getSimpleData(), $name)); + }; + } + +} diff --git a/src/Response/ExcelResponse.php b/src/Response/ExcelResponse.php new file mode 100644 index 000000000..293833fce --- /dev/null +++ b/src/Response/ExcelResponse.php @@ -0,0 +1,97 @@ +> */ + protected array $data; + + protected string $name; + + /** @var string[] */ + protected array $headers = [ + 'Expires' => '0', + 'Cache-Control' => 'no-cache', + 'Pragma' => 'Public', + ]; + + /** + * @param array> $data Input data + */ + public function __construct( + array $data, + string $name = 'export.xlsx', + ) + { + if (!str_contains($name, '.xlsx')) { + $name = sprintf('%s.xlsx', $name); + } + + $this->name = $name; + $this->data = $data; + } + + public function send(HttpRequest $httpRequest, HttpResponse $httpResponse): void + { + // Disable tracy bar + if (class_exists(Debugger::class)) { + Debugger::$productionMode = true; + } + + // Set Content-Type header + $httpResponse->setContentType(self::CONTENT_TYPE); + + // Set Content-Disposition header + $httpResponse->setHeader('Content-Disposition', sprintf('attachment; filename="%s"', $this->name)); + + // Set other headers + foreach ($this->headers as $key => $value) { + $httpResponse->setHeader($key, $value); + } + + if (function_exists('ob_start')) { + ob_start(); + } + + $spreadsheet = new Spreadsheet(); + $sheet = $spreadsheet->createSheet(0); + + foreach ($this->data as $_rowIndex => $_row) { + foreach ($_row as $_columnIndex => $_value) { + $sheet->setCellValue([$_columnIndex + 1, $_rowIndex + 1], $_value); + } + } + + $highestColumnIndex = !empty($this->data) ? count($this->data[0]) : 0; + + for ($col = 1; $col <= $highestColumnIndex; $col++) { + $columnLetter = Coordinate::stringFromColumnIndex($col); + $sheet->getColumnDimension($columnLetter)->setAutoSize(true); + } + + $spreadsheet->setActiveSheetIndex(0); + + $writer = new Xlsx($spreadsheet); + $writer->save('php://output'); + + if (function_exists('ob_end_flush')) { + ob_end_flush(); + } + } + +} From ba12196e1732251828f267e73c7a1f85352867ba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pavel=20Konvi=C4=8Dka?= Date: Thu, 12 Jun 2025 00:48:01 +0200 Subject: [PATCH 2/3] Fix: compatibility + casing of namespaces and class names --- src/Export/ExportExcel.php | 6 +++--- src/Response/ExcelResponse.php | 6 ++++-- tests/Cases/FilterTest.phpt | 2 +- 3 files changed, 8 insertions(+), 6 deletions(-) diff --git a/src/Export/ExportExcel.php b/src/Export/ExportExcel.php index aa4a96587..28d6262f6 100644 --- a/src/Export/ExportExcel.php +++ b/src/Export/ExportExcel.php @@ -2,7 +2,7 @@ namespace Contributte\Datagrid\Export; -use Contributte\DataGrid\DataGrid; +use Contributte\Datagrid\Datagrid; use Contributte\Datagrid\ExcelDataModel; use Contributte\Datagrid\Response\ExcelResponse; @@ -10,7 +10,7 @@ class ExportExcel extends Export { public function __construct( - DataGrid $grid, + Datagrid $grid, string $text, string $name, bool $filtered, @@ -32,7 +32,7 @@ private function getExportCallback(string $name): callable { return function ( array $data, - DataGrid $grid + Datagrid $grid ) use ($name): void { $columns = $this->getColumns(); diff --git a/src/Response/ExcelResponse.php b/src/Response/ExcelResponse.php index 293833fce..ed24fc03e 100644 --- a/src/Response/ExcelResponse.php +++ b/src/Response/ExcelResponse.php @@ -16,7 +16,7 @@ class ExcelResponse implements Response { - public const string CONTENT_TYPE = 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'; + public const CONTENT_TYPE = 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'; /** @var array> */ protected array $data; @@ -72,12 +72,14 @@ public function send(HttpRequest $httpRequest, HttpResponse $httpResponse): void $sheet = $spreadsheet->createSheet(0); foreach ($this->data as $_rowIndex => $_row) { + $_rowIndex = (int) $_rowIndex; + foreach ($_row as $_columnIndex => $_value) { $sheet->setCellValue([$_columnIndex + 1, $_rowIndex + 1], $_value); } } - $highestColumnIndex = !empty($this->data) ? count($this->data[0]) : 0; + $highestColumnIndex = count($this->data) > 0 ? count($this->data[0]) : 0; for ($col = 1; $col <= $highestColumnIndex; $col++) { $columnLetter = Coordinate::stringFromColumnIndex($col); diff --git a/tests/Cases/FilterTest.phpt b/tests/Cases/FilterTest.phpt index ba6bbed2f..841ef9e9a 100644 --- a/tests/Cases/FilterTest.phpt +++ b/tests/Cases/FilterTest.phpt @@ -35,7 +35,7 @@ final class FilterTest extends TestCase public function testFilterSubmitWithInvalidInlineAddOpen(): void { $factory = new TestingDataGridFactoryRouter(); - /** @var \Ublaboo\DataGrid\Datagrid $grid */ + /** @var Datagrid $grid */ $grid = $factory->createTestingDataGrid()->getComponent('grid'); $grid->addColumnText('status', 'Status'); From 04c7b4c00a6746c136f4bffb433132cd09a057d4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pavel=20Konvi=C4=8Dka?= Date: Thu, 12 Jun 2025 10:37:21 +0200 Subject: [PATCH 3/3] Refactor spreadsheet export to use new library --- composer.json | 4 ++-- src/Response/ExcelResponse.php | 29 ++++------------------------- 2 files changed, 6 insertions(+), 27 deletions(-) diff --git a/composer.json b/composer.json index 1c238ac30..2f935b316 100644 --- a/composer.json +++ b/composer.json @@ -25,12 +25,12 @@ ], "require": { "php": ">=8.2", + "mk-j/php_xlsxwriter": "^0.39.0", "nette/application": "^3.2.0", "nette/di": "^3.0.0", "nette/forms": "^3.2.0", "nette/utils": "^4.0.0", - "symfony/property-access": "^6.4.0 || ^7.2.0", - "phpoffice/phpspreadsheet": "^3.3" + "symfony/property-access": "^6.4.0 || ^7.2.0" }, "require-dev": { "contributte/qa": "^0.3.0", diff --git a/src/Response/ExcelResponse.php b/src/Response/ExcelResponse.php index ed24fc03e..bf9a311a7 100644 --- a/src/Response/ExcelResponse.php +++ b/src/Response/ExcelResponse.php @@ -5,10 +5,8 @@ use Nette\Application\Response; use Nette\Http\IRequest as HttpRequest; use Nette\Http\IResponse as HttpResponse; -use PhpOffice\PhpSpreadsheet\Cell\Coordinate; -use PhpOffice\PhpSpreadsheet\Spreadsheet; -use PhpOffice\PhpSpreadsheet\Writer\Xlsx; use Tracy\Debugger; +use XLSXWriter; /** * CSV file download response @@ -68,28 +66,9 @@ public function send(HttpRequest $httpRequest, HttpResponse $httpResponse): void ob_start(); } - $spreadsheet = new Spreadsheet(); - $sheet = $spreadsheet->createSheet(0); - - foreach ($this->data as $_rowIndex => $_row) { - $_rowIndex = (int) $_rowIndex; - - foreach ($_row as $_columnIndex => $_value) { - $sheet->setCellValue([$_columnIndex + 1, $_rowIndex + 1], $_value); - } - } - - $highestColumnIndex = count($this->data) > 0 ? count($this->data[0]) : 0; - - for ($col = 1; $col <= $highestColumnIndex; $col++) { - $columnLetter = Coordinate::stringFromColumnIndex($col); - $sheet->getColumnDimension($columnLetter)->setAutoSize(true); - } - - $spreadsheet->setActiveSheetIndex(0); - - $writer = new Xlsx($spreadsheet); - $writer->save('php://output'); + $writer = new XLSXWriter(); + $writer->writeSheet($this->data); + $writer->writeToStdOut(); if (function_exists('ob_end_flush')) { ob_end_flush();