Skip to content

Commit f3a35b3

Browse files
MaximePinotcurry684spyridonasfreezy-skrwkt
authored
Add server side export (#83)
* Add server side export * Add missing dev dependency * Replace pseudo-type "iterable" by "\Traversable" To support PHP 7.0 * Replace TranslatorInterface by DataCollectorTranslator To maintain BC, it is not possible to type hint with the TranslatorInterface as older versions of Symfony inject "Symfony\Component\Translation\TranslatorInterface" while newer versions of Symfony inject "Symfony\Contracts\Translation\TranslatorInterface". * Fix ArgumentCountError (Symfony ^3.4) * Fix Content Disposition header format * Do not display the export form * Excel: render HTML in cells The 'render' column option is usually use to inject HTML. * Fix Translator injection Tests were passing using "Symfony\Component\Translation\DataCollectorTranslator" as type-hint but Symfony may inject "Symfony\Bundle\FrameworkBundle\Translation\Translator". * Use a Generator instead of a new Iterator * Using a Generator makes the code more readable. * Avoid creating an array (iterator_to_array) may be sligtly faster? * Add functional test "testEmptyDataTable" * Fix tests * Preserve DataTableState Export the entire table but keep current sort and filtering * Automatically tag exporters * Add CSV exporter * Add missing "_dt" * Fix "DT_RowId" check * Make dependency on contracts explicit Will not fix #122 but should prevent similar issues. * Symfony 3 is not supported, according to the readme.md (#123) * Symfony 3 is not supported, according to the readme.md Symfony 3 is not supported, according to the readme.md. This updates the documentation to reflect that only Symfony 4.1+ is supported by this bundle * Update index.html.md * Option for custom datetime format for creating object (#127) * Fix failing test by reverting custom WebClient handling Alternative to #125 * Fix deprecations helper crashing old Symfony versions * Drop bad testing method on old Symfony frameworks This is actually obsolete since Symfony Flex, as you can now run Symfony 4.1 with more recent components. * Apply code style * Use weak deprecation testing * Prepare 0.4.1 * Fix deprecations (#129) * removed unused symfony/templating * set lowest version of persistence to 1.3.4 * added missing SymfonyTestListener * raise minimum version of doctrine/orm to 2.6.3 * set phpunit schema location to installed package * Update translations with script (#130) * script for updating translation messages from datatables language files * added czech and slovak languages * en rebuilt by script * add missing closing bracket for example (#131) * Integrate translation update script * Update README.md * Restructure tests to improve readability * Bump nokogiri from 1.10.7 to 1.10.8 in /docs (#133) Bumps [nokogiri](https://github.com/sparklemotion/nokogiri) from 1.10.7 to 1.10.8. - [Release notes](https://github.com/sparklemotion/nokogiri/releases) - [Changelog](https://github.com/sparklemotion/nokogiri/blob/master/CHANGELOG.md) - [Commits](sparklemotion/nokogiri@v1.10.7...v1.10.8) Signed-off-by: dependabot[bot] <[email protected]> * Test local translations (#139) * Upgrade to PHPUnit 8/9 * Update Scrutinizer config * Force use outdated version of ocramius/package-versions Breaks both SymfonyInsight and Scrutinizer otherwise. * Fix SymfonyInsight badge * Fix SymfonyInsight badge for real * Javascript improvements (#145) * JS improvements * Remove scrollX * Allow config.options and config.url as functions * Remove empty line * Fix export issue when using GET method * Add server side export * Add missing dev dependency * Replace pseudo-type "iterable" by "\Traversable" To support PHP 7.0 * Replace TranslatorInterface by DataCollectorTranslator To maintain BC, it is not possible to type hint with the TranslatorInterface as older versions of Symfony inject "Symfony\Component\Translation\TranslatorInterface" while newer versions of Symfony inject "Symfony\Contracts\Translation\TranslatorInterface". * Fix ArgumentCountError (Symfony ^3.4) * Fix Content Disposition header format * Do not display the export form * Excel: render HTML in cells The 'render' column option is usually use to inject HTML. * Fix Translator injection Tests were passing using "Symfony\Component\Translation\DataCollectorTranslator" as type-hint but Symfony may inject "Symfony\Bundle\FrameworkBundle\Translation\Translator". * Use a Generator instead of a new Iterator * Using a Generator makes the code more readable. * Avoid creating an array (iterator_to_array) may be sligtly faster? * Add functional test "testEmptyDataTable" * Fix tests * Preserve DataTableState Export the entire table but keep current sort and filtering * Automatically tag exporters * Add CSV exporter * Add missing "_dt" * Fix "DT_RowId" check * Fix export issue when using GET method * Fix tests Co-authored-by: Niels Keurentjes <[email protected]> Co-authored-by: Spyros Sakellaropoulos <[email protected]> Co-authored-by: freezy <[email protected]> Co-authored-by: rwkt <[email protected]> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Daniel Gorgan <[email protected]>
1 parent 6da1572 commit f3a35b3

32 files changed

+1192
-19
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,4 @@
22
/composer.lock
33
/tmp
44
/vendor
5+
/tests/Fixtures/var/

composer.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@
3737
"doctrine/persistence": "^1.3.4",
3838
"friendsofphp/php-cs-fixer": "^2.7",
3939
"mongodb/mongodb": "^1.2",
40+
"phpoffice/phpspreadsheet": "^1.6",
4041
"ocramius/package-versions": "1.4.*",
4142
"phpunit/phpunit": "^8.5|^9.0",
4243
"ruflin/elastica": "^6.0|^7.0",
@@ -54,7 +55,8 @@
5455
"doctrine/orm": "For full automated integration with Doctrine entities",
5556
"mongodb/mongodb": "For integration with MongoDB collections",
5657
"ruflin/elastica": "For integration with Elasticsearch indexes",
57-
"symfony/twig-bundle": "To use default Twig based rendering and TwigColumn"
58+
"symfony/twig-bundle": "To use default Twig based rendering and TwigColumn",
59+
"phpoffice/phpspreadsheet": "To export the data from DataTables to Excel"
5860
},
5961
"autoload": {
6062
"psr-4": { "Omines\\DataTablesBundle\\": "src/"}

src/DataTable.php

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,11 @@
1919
use Omines\DataTablesBundle\Exception\InvalidArgumentException;
2020
use Omines\DataTablesBundle\Exception\InvalidConfigurationException;
2121
use Omines\DataTablesBundle\Exception\InvalidStateException;
22+
use Omines\DataTablesBundle\Exporter\DataTableExporterManager;
2223
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
2324
use Symfony\Component\HttpFoundation\JsonResponse;
2425
use Symfony\Component\HttpFoundation\Request;
26+
use Symfony\Component\HttpFoundation\Response;
2527
use Symfony\Component\OptionsResolver\OptionsResolver;
2628

2729
/**
@@ -69,6 +71,9 @@ class DataTable
6971
/** @var EventDispatcherInterface */
7072
protected $eventDispatcher;
7173

74+
/** @var DataTableExporterManager */
75+
protected $exporterManager;
76+
7277
/** @var string */
7378
protected $method = Request::METHOD_POST;
7479

@@ -107,10 +112,16 @@ class DataTable
107112

108113
/**
109114
* DataTable constructor.
115+
*
116+
* @param EventDispatcherInterface $eventDispatcher
117+
* @param DataTableExporterManager $exporterManager
118+
* @param array $options
119+
* @param Instantiator|null $instantiator
110120
*/
111-
public function __construct(EventDispatcherInterface $eventDispatcher, array $options = [], Instantiator $instantiator = null)
121+
public function __construct(EventDispatcherInterface $eventDispatcher, DataTableExporterManager $exporterManager, array $options = [], Instantiator $instantiator = null)
112122
{
113123
$this->eventDispatcher = $eventDispatcher;
124+
$this->exporterManager = $exporterManager;
114125

115126
$this->instantiator = $instantiator ?? new Instantiator();
116127

@@ -277,12 +288,20 @@ public function handleRequest(Request $request): self
277288
return $this;
278289
}
279290

280-
public function getResponse(): JsonResponse
291+
public function getResponse(): Response
281292
{
282293
if (null === $this->state) {
283294
throw new InvalidStateException('The DataTable does not know its state yet, did you call handleRequest?');
284295
}
285296

297+
// Server side export
298+
if (null !== $this->state->getExporterName()) {
299+
return $this->exporterManager
300+
->setDataTable($this)
301+
->setExporterName($this->state->getExporterName())
302+
->getResponse();
303+
}
304+
286305
$resultSet = $this->getResultSet();
287306
$response = [
288307
'draw' => $this->state->getDraw(),

src/DataTableFactory.php

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
namespace Omines\DataTablesBundle;
1414

1515
use Omines\DataTablesBundle\DependencyInjection\Instantiator;
16+
use Omines\DataTablesBundle\Exporter\DataTableExporterManager;
1617
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
1718
use Symfony\Component\HttpFoundation\Request;
1819

@@ -33,15 +34,19 @@ class DataTableFactory
3334
/** @var EventDispatcherInterface */
3435
protected $eventDispatcher;
3536

37+
/** @var DataTableExporterManager */
38+
protected $exporterManager;
39+
3640
/**
3741
* DataTableFactory constructor.
3842
*/
39-
public function __construct(array $config, DataTableRendererInterface $renderer, Instantiator $instantiator, EventDispatcherInterface $eventDispatcher)
43+
public function __construct(array $config, DataTableRendererInterface $renderer, Instantiator $instantiator, EventDispatcherInterface $eventDispatcher, DataTableExporterManager $exporterManager)
4044
{
4145
$this->config = $config;
4246
$this->renderer = $renderer;
4347
$this->instantiator = $instantiator;
4448
$this->eventDispatcher = $eventDispatcher;
49+
$this->exporterManager = $exporterManager;
4550
}
4651

4752
/**
@@ -51,7 +56,7 @@ public function create(array $options = [])
5156
{
5257
$config = $this->config;
5358

54-
return (new DataTable($this->eventDispatcher, array_merge($config['options'] ?? [], $options), $this->instantiator))
59+
return (new DataTable($this->eventDispatcher, $this->exporterManager, array_merge($config['options'] ?? [], $options), $this->instantiator))
5560
->setRenderer($this->renderer)
5661
->setMethod($config['method'] ?? Request::METHOD_POST)
5762
->setPersistState($config['persist_state'] ?? 'fragment')

src/DataTableState.php

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,9 @@ class DataTableState
4949
/** @var bool */
5050
private $isCallback = false;
5151

52+
/** @var string */
53+
private $exporterName = null;
54+
5255
/**
5356
* DataTableState constructor.
5457
*/
@@ -83,6 +86,7 @@ public function applyParameters(ParameterBag $parameters)
8386
$this->draw = $parameters->getInt('draw');
8487
$this->isCallback = true;
8588
$this->isInitial = $parameters->getBoolean('_init', false);
89+
$this->exporterName = $parameters->get('_exporter');
8690

8791
$this->start = (int) $parameters->get('start', $this->start);
8892
$this->length = (int) $parameters->get('length', $this->length);
@@ -224,4 +228,12 @@ public function setColumnSearch(AbstractColumn $column, string $search, bool $is
224228

225229
return $this;
226230
}
231+
232+
/**
233+
* @return string
234+
*/
235+
public function getExporterName()
236+
{
237+
return $this->exporterName;
238+
}
227239
}

src/DependencyInjection/Compiler/LocatorRegistrationPass.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
use Omines\DataTablesBundle\Column\AbstractColumn;
1717
use Omines\DataTablesBundle\DataTableTypeInterface;
1818
use Omines\DataTablesBundle\DependencyInjection\Instantiator;
19+
use Omines\DataTablesBundle\Exporter\DataTableExporterInterface;
1920
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
2021
use Symfony\Component\DependencyInjection\ContainerBuilder;
2122
use Symfony\Component\DependencyInjection\Definition;
@@ -39,6 +40,7 @@ public function process(ContainerBuilder $container)
3940
AdapterInterface::class => $this->registerLocator($container, 'adapter'),
4041
AbstractColumn::class => $this->registerLocator($container, 'column'),
4142
DataTableTypeInterface::class => $this->registerLocator($container, 'type'),
43+
DataTableExporterInterface::class => $this->registerLocator($container, 'exporter'),
4244
]]);
4345
}
4446

src/DependencyInjection/DataTablesExtension.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
use Omines\DataTablesBundle\Adapter\AdapterInterface;
1616
use Omines\DataTablesBundle\Column\AbstractColumn;
1717
use Omines\DataTablesBundle\DataTableTypeInterface;
18+
use Omines\DataTablesBundle\Exporter\DataTableExporterInterface;
1819
use Omines\DataTablesBundle\Filter\AbstractFilter;
1920
use Symfony\Component\Config\FileLocator;
2021
use Symfony\Component\DependencyInjection\ContainerBuilder;
@@ -55,6 +56,8 @@ public function load(array $configs, ContainerBuilder $container)
5556
->setShared(false);
5657
$container->registerForAutoconfiguration(DataTableTypeInterface::class)
5758
->addTag('datatables.type');
59+
$container->registerForAutoconfiguration(DataTableExporterInterface::class)
60+
->addTag('datatables.exporter');
5861
}
5962

6063
/**
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
<?php
2+
3+
/*
4+
* Symfony DataTables Bundle
5+
* (c) Omines Internetbureau B.V. - https://omines.nl/
6+
*
7+
* For the full copyright and license information, please view the LICENSE
8+
* file that was distributed with this source code.
9+
*/
10+
11+
declare(strict_types=1);
12+
13+
namespace Omines\DataTablesBundle\Exception;
14+
15+
use Omines\DataTablesBundle\Exporter\DataTableExporterInterface;
16+
17+
/**
18+
* Thrown when a DataTable exporter cannot be found.
19+
*
20+
* @author Maxime Pinot <[email protected]>
21+
*/
22+
class UnknownDataTableExporterException extends \Exception
23+
{
24+
/**
25+
* UnknownDataTableExporterException constructor.
26+
*
27+
* @param string $name The name of the DataTable exporter that cannot be found
28+
* @param \Traversable $referencedExporters Available DataTable exporters
29+
*/
30+
public function __construct(string $name, \Traversable $referencedExporters)
31+
{
32+
$format = <<<EOF
33+
Cannot find a DataTable exporter named "%s".
34+
Did you forget to tag the exporter with 'datatables.exporter'?
35+
Referenced DataTable exporters are : %s.
36+
EOF;
37+
38+
parent::__construct(sprintf($format, $name, $this->formatReferencedExporters($referencedExporters)));
39+
}
40+
41+
private function formatReferencedExporters(\Traversable $referencedExporters): string
42+
{
43+
$names = [];
44+
45+
/** @var DataTableExporterInterface $exporter */
46+
foreach ($referencedExporters as $exporter) {
47+
$names[] = sprintf('"%s"', $exporter->getName());
48+
}
49+
50+
return implode(', ', $names);
51+
}
52+
}

src/Exporter/Csv/CsvExporter.php

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Omines\DataTablesBundle\Exporter\Csv;
6+
7+
use Omines\DataTablesBundle\Exporter\DataTableExporterInterface;
8+
9+
/**
10+
* Exports DataTable data to a CSV file.
11+
*
12+
* @author Maxime Pinot <[email protected]>
13+
*/
14+
class CsvExporter implements DataTableExporterInterface
15+
{
16+
/**
17+
* {@inheritdoc}
18+
*/
19+
public function export(array $columnNames, \Iterator $data): \SplFileInfo
20+
{
21+
$filePath = sys_get_temp_dir() . '/' . uniqid('dt') . '.csv';
22+
23+
$file = fopen($filePath, 'w');
24+
25+
fputcsv($file, $columnNames);
26+
27+
foreach ($data as $row) {
28+
fputcsv($file, array_map('strip_tags', $row));
29+
}
30+
31+
fclose($file);
32+
33+
return new \SplFileInfo($filePath);
34+
}
35+
36+
/**
37+
* {@inheritdoc}
38+
*/
39+
public function getName(): string
40+
{
41+
return 'csv';
42+
}
43+
}
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
<?php
2+
3+
/*
4+
* Symfony DataTables Bundle
5+
* (c) Omines Internetbureau B.V. - https://omines.nl/
6+
*
7+
* For the full copyright and license information, please view the LICENSE
8+
* file that was distributed with this source code.
9+
*/
10+
11+
declare(strict_types=1);
12+
13+
namespace Omines\DataTablesBundle\Exporter;
14+
15+
use Omines\DataTablesBundle\Exception\UnknownDataTableExporterException;
16+
17+
/**
18+
* Holds the available DataTable exporters.
19+
* Exporters must be tagged with 'datatables.exporter'.
20+
*
21+
* @author Maxime Pinot <[email protected]>
22+
*/
23+
class DataTableExporterCollection
24+
{
25+
/** @var \Traversable The available exporters */
26+
private $exporters;
27+
28+
/**
29+
* DataTableExporterCollection constructor.
30+
*
31+
* @param \Traversable $exporters
32+
*/
33+
public function __construct(\Traversable $exporters)
34+
{
35+
$this->exporters = $exporters;
36+
}
37+
38+
/**
39+
* Finds a DataTable exporter that matches the given name.
40+
*
41+
* @param string $name
42+
*
43+
* @return DataTableExporterInterface
44+
*
45+
* @throws UnknownDataTableExporterException
46+
*/
47+
public function getByName(string $name): DataTableExporterInterface
48+
{
49+
foreach ($this->exporters as $exporter) {
50+
if ($exporter->getName() === $name) {
51+
return $exporter;
52+
}
53+
}
54+
55+
throw new UnknownDataTableExporterException($name, $this->exporters);
56+
}
57+
}

0 commit comments

Comments
 (0)