Skip to content

Commit

Permalink
Add server side export (#83)
Browse files Browse the repository at this point in the history
* 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] <support@github.com>

* 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 <niels.keurentjes@omines.com>
Co-authored-by: Spyros Sakellaropoulos <spyridonas@users.noreply.github.com>
Co-authored-by: freezy <freezy-sk@users.noreply.github.com>
Co-authored-by: rwkt <37826694+rwkt@users.noreply.github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Daniel Gorgan <danut007ro@gmail.com>
  • Loading branch information
7 people committed Apr 28, 2020
1 parent 6da1572 commit f3a35b3
Show file tree
Hide file tree
Showing 32 changed files with 1,192 additions and 19 deletions.
1 change: 1 addition & 0 deletions .gitignore
Expand Up @@ -2,3 +2,4 @@
/composer.lock
/tmp
/vendor
/tests/Fixtures/var/
4 changes: 3 additions & 1 deletion composer.json
Expand Up @@ -37,6 +37,7 @@
"doctrine/persistence": "^1.3.4",
"friendsofphp/php-cs-fixer": "^2.7",
"mongodb/mongodb": "^1.2",
"phpoffice/phpspreadsheet": "^1.6",
"ocramius/package-versions": "1.4.*",
"phpunit/phpunit": "^8.5|^9.0",
"ruflin/elastica": "^6.0|^7.0",
Expand All @@ -54,7 +55,8 @@
"doctrine/orm": "For full automated integration with Doctrine entities",
"mongodb/mongodb": "For integration with MongoDB collections",
"ruflin/elastica": "For integration with Elasticsearch indexes",
"symfony/twig-bundle": "To use default Twig based rendering and TwigColumn"
"symfony/twig-bundle": "To use default Twig based rendering and TwigColumn",
"phpoffice/phpspreadsheet": "To export the data from DataTables to Excel"
},
"autoload": {
"psr-4": { "Omines\\DataTablesBundle\\": "src/"}
Expand Down
23 changes: 21 additions & 2 deletions src/DataTable.php
Expand Up @@ -19,9 +19,11 @@
use Omines\DataTablesBundle\Exception\InvalidArgumentException;
use Omines\DataTablesBundle\Exception\InvalidConfigurationException;
use Omines\DataTablesBundle\Exception\InvalidStateException;
use Omines\DataTablesBundle\Exporter\DataTableExporterManager;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\OptionsResolver\OptionsResolver;

/**
Expand Down Expand Up @@ -69,6 +71,9 @@ class DataTable
/** @var EventDispatcherInterface */
protected $eventDispatcher;

/** @var DataTableExporterManager */
protected $exporterManager;

/** @var string */
protected $method = Request::METHOD_POST;

Expand Down Expand Up @@ -107,10 +112,16 @@ class DataTable

/**
* DataTable constructor.
*
* @param EventDispatcherInterface $eventDispatcher
* @param DataTableExporterManager $exporterManager
* @param array $options
* @param Instantiator|null $instantiator
*/
public function __construct(EventDispatcherInterface $eventDispatcher, array $options = [], Instantiator $instantiator = null)
public function __construct(EventDispatcherInterface $eventDispatcher, DataTableExporterManager $exporterManager, array $options = [], Instantiator $instantiator = null)
{
$this->eventDispatcher = $eventDispatcher;
$this->exporterManager = $exporterManager;

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

Expand Down Expand Up @@ -277,12 +288,20 @@ public function handleRequest(Request $request): self
return $this;
}

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

// Server side export
if (null !== $this->state->getExporterName()) {
return $this->exporterManager
->setDataTable($this)
->setExporterName($this->state->getExporterName())
->getResponse();
}

$resultSet = $this->getResultSet();
$response = [
'draw' => $this->state->getDraw(),
Expand Down
9 changes: 7 additions & 2 deletions src/DataTableFactory.php
Expand Up @@ -13,6 +13,7 @@
namespace Omines\DataTablesBundle;

use Omines\DataTablesBundle\DependencyInjection\Instantiator;
use Omines\DataTablesBundle\Exporter\DataTableExporterManager;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\HttpFoundation\Request;

Expand All @@ -33,15 +34,19 @@ class DataTableFactory
/** @var EventDispatcherInterface */
protected $eventDispatcher;

/** @var DataTableExporterManager */
protected $exporterManager;

/**
* DataTableFactory constructor.
*/
public function __construct(array $config, DataTableRendererInterface $renderer, Instantiator $instantiator, EventDispatcherInterface $eventDispatcher)
public function __construct(array $config, DataTableRendererInterface $renderer, Instantiator $instantiator, EventDispatcherInterface $eventDispatcher, DataTableExporterManager $exporterManager)
{
$this->config = $config;
$this->renderer = $renderer;
$this->instantiator = $instantiator;
$this->eventDispatcher = $eventDispatcher;
$this->exporterManager = $exporterManager;
}

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

return (new DataTable($this->eventDispatcher, array_merge($config['options'] ?? [], $options), $this->instantiator))
return (new DataTable($this->eventDispatcher, $this->exporterManager, array_merge($config['options'] ?? [], $options), $this->instantiator))
->setRenderer($this->renderer)
->setMethod($config['method'] ?? Request::METHOD_POST)
->setPersistState($config['persist_state'] ?? 'fragment')
Expand Down
12 changes: 12 additions & 0 deletions src/DataTableState.php
Expand Up @@ -49,6 +49,9 @@ class DataTableState
/** @var bool */
private $isCallback = false;

/** @var string */
private $exporterName = null;

/**
* DataTableState constructor.
*/
Expand Down Expand Up @@ -83,6 +86,7 @@ public function applyParameters(ParameterBag $parameters)
$this->draw = $parameters->getInt('draw');
$this->isCallback = true;
$this->isInitial = $parameters->getBoolean('_init', false);
$this->exporterName = $parameters->get('_exporter');

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

return $this;
}

/**
* @return string
*/
public function getExporterName()
{
return $this->exporterName;
}
}
2 changes: 2 additions & 0 deletions src/DependencyInjection/Compiler/LocatorRegistrationPass.php
Expand Up @@ -16,6 +16,7 @@
use Omines\DataTablesBundle\Column\AbstractColumn;
use Omines\DataTablesBundle\DataTableTypeInterface;
use Omines\DataTablesBundle\DependencyInjection\Instantiator;
use Omines\DataTablesBundle\Exporter\DataTableExporterInterface;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Definition;
Expand All @@ -39,6 +40,7 @@ public function process(ContainerBuilder $container)
AdapterInterface::class => $this->registerLocator($container, 'adapter'),
AbstractColumn::class => $this->registerLocator($container, 'column'),
DataTableTypeInterface::class => $this->registerLocator($container, 'type'),
DataTableExporterInterface::class => $this->registerLocator($container, 'exporter'),
]]);
}

Expand Down
3 changes: 3 additions & 0 deletions src/DependencyInjection/DataTablesExtension.php
Expand Up @@ -15,6 +15,7 @@
use Omines\DataTablesBundle\Adapter\AdapterInterface;
use Omines\DataTablesBundle\Column\AbstractColumn;
use Omines\DataTablesBundle\DataTableTypeInterface;
use Omines\DataTablesBundle\Exporter\DataTableExporterInterface;
use Omines\DataTablesBundle\Filter\AbstractFilter;
use Symfony\Component\Config\FileLocator;
use Symfony\Component\DependencyInjection\ContainerBuilder;
Expand Down Expand Up @@ -55,6 +56,8 @@ public function load(array $configs, ContainerBuilder $container)
->setShared(false);
$container->registerForAutoconfiguration(DataTableTypeInterface::class)
->addTag('datatables.type');
$container->registerForAutoconfiguration(DataTableExporterInterface::class)
->addTag('datatables.exporter');
}

/**
Expand Down
52 changes: 52 additions & 0 deletions src/Exception/UnknownDataTableExporterException.php
@@ -0,0 +1,52 @@
<?php

/*
* Symfony DataTables Bundle
* (c) Omines Internetbureau B.V. - https://omines.nl/
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

declare(strict_types=1);

namespace Omines\DataTablesBundle\Exception;

use Omines\DataTablesBundle\Exporter\DataTableExporterInterface;

/**
* Thrown when a DataTable exporter cannot be found.
*
* @author Maxime Pinot <contact@maximepinot.com>
*/
class UnknownDataTableExporterException extends \Exception
{
/**
* UnknownDataTableExporterException constructor.
*
* @param string $name The name of the DataTable exporter that cannot be found
* @param \Traversable $referencedExporters Available DataTable exporters
*/
public function __construct(string $name, \Traversable $referencedExporters)
{
$format = <<<EOF
Cannot find a DataTable exporter named "%s".
Did you forget to tag the exporter with 'datatables.exporter'?
Referenced DataTable exporters are : %s.
EOF;

parent::__construct(sprintf($format, $name, $this->formatReferencedExporters($referencedExporters)));
}

private function formatReferencedExporters(\Traversable $referencedExporters): string
{
$names = [];

/** @var DataTableExporterInterface $exporter */
foreach ($referencedExporters as $exporter) {
$names[] = sprintf('"%s"', $exporter->getName());
}

return implode(', ', $names);
}
}
43 changes: 43 additions & 0 deletions src/Exporter/Csv/CsvExporter.php
@@ -0,0 +1,43 @@
<?php

declare(strict_types=1);

namespace Omines\DataTablesBundle\Exporter\Csv;

use Omines\DataTablesBundle\Exporter\DataTableExporterInterface;

/**
* Exports DataTable data to a CSV file.
*
* @author Maxime Pinot <maxime.pinot@gbh.fr>
*/
class CsvExporter implements DataTableExporterInterface
{
/**
* {@inheritdoc}
*/
public function export(array $columnNames, \Iterator $data): \SplFileInfo
{
$filePath = sys_get_temp_dir() . '/' . uniqid('dt') . '.csv';

$file = fopen($filePath, 'w');

fputcsv($file, $columnNames);

foreach ($data as $row) {
fputcsv($file, array_map('strip_tags', $row));
}

fclose($file);

return new \SplFileInfo($filePath);
}

/**
* {@inheritdoc}
*/
public function getName(): string
{
return 'csv';
}
}
57 changes: 57 additions & 0 deletions src/Exporter/DataTableExporterCollection.php
@@ -0,0 +1,57 @@
<?php

/*
* Symfony DataTables Bundle
* (c) Omines Internetbureau B.V. - https://omines.nl/
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

declare(strict_types=1);

namespace Omines\DataTablesBundle\Exporter;

use Omines\DataTablesBundle\Exception\UnknownDataTableExporterException;

/**
* Holds the available DataTable exporters.
* Exporters must be tagged with 'datatables.exporter'.
*
* @author Maxime Pinot <contact@maximepinot.com>
*/
class DataTableExporterCollection
{
/** @var \Traversable The available exporters */
private $exporters;

/**
* DataTableExporterCollection constructor.
*
* @param \Traversable $exporters
*/
public function __construct(\Traversable $exporters)
{
$this->exporters = $exporters;
}

/**
* Finds a DataTable exporter that matches the given name.
*
* @param string $name
*
* @return DataTableExporterInterface
*
* @throws UnknownDataTableExporterException
*/
public function getByName(string $name): DataTableExporterInterface
{
foreach ($this->exporters as $exporter) {
if ($exporter->getName() === $name) {
return $exporter;
}
}

throw new UnknownDataTableExporterException($name, $this->exporters);
}
}
32 changes: 32 additions & 0 deletions src/Exporter/DataTableExporterEvents.php
@@ -0,0 +1,32 @@
<?php

/*
* Symfony DataTables Bundle
* (c) Omines Internetbureau B.V. - https://omines.nl/
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

declare(strict_types=1);

namespace Omines\DataTablesBundle\Exporter;

/**
* Available events.
*
* @author Maxime Pinot <contact@maximepinot.com>
*/
final class DataTableExporterEvents
{
/**
* The PRE_RESPONSE event is dispatched before sending
* the BinaryFileResponse to the user.
*
* Note that the file is accessible through the Response object.
* Both the file and the Response can be modified before being sent.
*
* @Event("Omines\DataTablesBundle\Exporter\Event\DataTableExporterResponseEvent")
*/
const PRE_RESPONSE = 'omines_datatables.exporter.pre_response';
}

0 comments on commit f3a35b3

Please sign in to comment.