Skip to content

feat: Excel export #1175

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
],
"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",
Expand Down
16 changes: 15 additions & 1 deletion src/Datagrid.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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);
Expand Down
61 changes: 61 additions & 0 deletions src/ExcelDataModel.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
<?php declare(strict_types = 1);

namespace Contributte\Datagrid;

use Contributte\Datagrid\Column\Column;
use Nette\Localization\Translator;

class ExcelDataModel
{

/**
* @param Column[] $columns
*/
public function __construct(protected array $data, protected array $columns, protected Translator $translator)
{
}

/**
* Get data with header and "body"
*/
public function getSimpleData(bool $includeHeader = true): array
{
$return = [];

if ($includeHeader) {
$return[] = $this->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;
}

}
49 changes: 49 additions & 0 deletions src/Export/ExportExcel.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
<?php declare(strict_types = 1);

namespace Contributte\Datagrid\Export;

use Contributte\Datagrid\Datagrid;
use Contributte\Datagrid\ExcelDataModel;
use Contributte\Datagrid\Response\ExcelResponse;

class ExportExcel extends Export
{

public function __construct(
Datagrid $grid,
string $text,
string $name,
bool $filtered,
)
{
if (!str_contains($name, '.xlsx')) {
$name .= '.xlsx';
}

parent::__construct(
$grid,
$text,
$this->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));
};
}

}
78 changes: 78 additions & 0 deletions src/Response/ExcelResponse.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
<?php declare(strict_types = 1);

namespace Contributte\Datagrid\Response;

use Nette\Application\Response;
use Nette\Http\IRequest as HttpRequest;
use Nette\Http\IResponse as HttpResponse;
use Tracy\Debugger;
use XLSXWriter;

/**
* CSV file download response
*/
class ExcelResponse implements Response
{

public const CONTENT_TYPE = 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet';

/** @var array<int|string, array<scalar>> */
protected array $data;

protected string $name;

/** @var string[] */
protected array $headers = [
'Expires' => '0',
'Cache-Control' => 'no-cache',
'Pragma' => 'Public',
];

/**
* @param array<int|string, array<scalar>> $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();
}

$writer = new XLSXWriter();
$writer->writeSheet($this->data);
$writer->writeToStdOut();

if (function_exists('ob_end_flush')) {
ob_end_flush();
}
}

}
2 changes: 1 addition & 1 deletion tests/Cases/FilterTest.phpt
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand Down