prodevcon / data-exporter-bundle
Data exporter library bundle for Symfony framework
Requires
- symfony/options-resolver: ^4.4
- symfony/property-access: ^4.4
Requires (Dev)
- asimlqt/php-google-spreadsheet-client: ^2.3
- google/apiclient: ^1.1
- phpoffice/phpexcel: 1.8
- symfony/framework-bundle: ^4.4
- twig/twig: ^2.12.1
Suggests
- asimlqt/php-google-spreadsheet-client: To use the Google Spreadsheet output adapter
- phpoffice/phpexcel: To use the PHPExcel output adapter
This package is not auto-updated.
Last update: 2025-03-13 09:35:48 UTC
README
Note: The API is not stable and is subject to change.
Data Exporter Bundle for Symfony.
Assume the following table, which contains 2 user profile objects:
id | firstName | lastName | age |
---|---|---|---|
1 | Foo | Chan | 20 |
2 | Bar | Wong | 17 |
The data structure would look like this in code:
$items = [
['id' => 1, 'firstName' => 'Foo', 'lastName' => 'Chan', 'age' => 20],
['id' => 2, 'firstName' => 'Bar', 'lastName' => 'Wong', 'age' => 17],
];
With this bundle you can transform the data set ($items
) to another format, like CSV:
Id,FirstName,LastName,Age
1,Foo,Chan,20
2,Bar,Wong,17
Basic Usage
First you need to enable the bundle in AppKernel.php
:
$bundles = array(
// ....
new Sparkson\DataExporterBundle\SparksonDataExporterBundle(),
);
Now assume you are in a controller action. Here is how to use the exporter:
public function exportAction(Request $request)
{
// First, retrieve the ExporterFactory service.
$exporterFactory = $this->get('sparkson.data_exporter.factory');
// Then create a new exporter builder instance.
$builder = $exporterFactory->createBuilder();
// We'll use the builder to build an exporter.
// Let's define the column structure.
$builder
->add('id', 'string')
->add('firstName', 'string')
->add('lastName', 'string')
->add('age', 'string');
// Build the exporter.
$exporter = $builder->getExporter();
// Assigns an output adapter. Here we use the CSV adapter that utilizes PHP's fputcsv() function.
$exporter->setOutputAdapter(new \Sparkson\DataExporterBundle\Exporter\OutputAdapter\CSVAdapter());
// Finally, sets the variable containing the data set to export ($items is assumed here).
$exporter->setDataSet($items);
// Run!
$exporter->execute();
// By default, the output adapter will write the result to a variable which can be retrieved via getResult().
$result = $exporter->getResult();
// Feed the result to the response object
return new Response($result);
}
The $result
variable will look like as follow:
Id,FirstName,LastName,Age
1,Foo,Chan,20
2,Bar,Wong,17
This illustrates the basic usage of the exporter. Note that methods can be chained as below:
$result = $builder
->add('id', 'string')
->add('firstName', 'string')
->add('lastName', 'string')
->add('age', 'string')
->getExporter() // this returns the Exporter instance
->setOutputAdapter(new \Sparkson\DataExporterBundle\Exporter\OutputAdapter\CSVAdapter());
->setDataSet($items)
->execute()
->getResult();
Defining the column structure
The exporter builder allows you to define the column set to be exported using $builder->add()
. For example:
$builder->add('id', 'string', []);
Users of the great Symfony Form component will find this syntax familiar. This is intentional.
- The first parameter is the field name, which has to be unique among the same column set.
- The second parameter is the field type. It can be a type name registered in the type registry (normally via the service tag), or an object implementing
Sparkson\DataExporterBundle\Exporter\Type\ExporterTypeInterface
. This means that instead of passing the type name you could also write$builder->add('id', new \Sparkson\DataExporterBundle\Exporter\Core\Type\StringType())
and the result will be the same. - The third parameter is the options.
The options
argument allows you to further configure the behaviour of a type. The allowed options are different among field types, with a few exceptions like label
and property_path
.
label
Each column has a label, which is used as the caption/title in the header row. If unspecified, the label will be generated from the column name.
property_path
The property_path
attribute overrides the default behaviour of using field name as the property path to retrieve the column values For example, you may want to write this when the record is an array:
$builder->add('id', 'string', ['property_path' => '[id]'])
A field type does not retrieve a column value by itself. Instead it passes the property_path
value to the value resolver component and doesn't care how the value is retrieved. By default, \Sparkson\DataExporterBundle\Exporter\ValueResolver\DefaultValueResolver
is used which fetches record values with the help of Symfony's PropertyAccess component. This means that you can write the following:
$builder->add('author_name', 'string', ['property_path' => 'author.name'])
Although it seems unlikely that you will want to roll your own value resolver, it is possible to tell the exporter to use your own value resolver by calling Exporter::setValueResolver()
.
resolver_options
Additional options can be passed to the active value resolver for each column via the resolver_options
key.
DefaultValueResolver
supports a filters
option which accepts an array of either
- a string which is a simple PHP function that accepts the exported value as the first parameter; or
- an instance implementing
Sparkson\DataExporterBundle\Exporter\ValueResolver\Filter\FilterInterface
. One example is theCustomFilter
class in this library.
Filters serve like value pre-processors, mainly for sanitizing values using functions like trim()
, ltrim()
etc. after the value is retrieved and before the value is passed it to the field type for final output.
$builder->add('description', 'string', ['resolver_options' => ['filters' => ['trim']]]);
// is equivalent to
$builder->add('description', 'string', ['resolver_options' => ['filters' => [ new CustomFilter(function($value) {
return trim($value);
})]]]);
Field types
Below are the field types provided in this bundle. Classes are defined under the Sparkson\DataExporterBundle\Exporter\Core\Type
namespace.
Class name | Type |
---|---|
StringType | string |
BooleanType | boolean |
CallbackType | callback |
DateTimeType | datetime |
MapType | map |
NumberType | number |
RawType | raw |
Here shows the use of the MapType
:
$builder->add('user_type', 'map', ['map' => [
'U' => 'User',
'A' => 'Administrator',
'M' => 'Moderator',
]]);
To put it another way, field types are like value transformers:
MapType
expects a string which should be a key of the provided map. During export,MapType
transforms the column value to the mapped value. Similarly,BooleanType
expects a boolean and will transform it into strings like "Yes/No", "Enabled/Disabled" depending on the configuration.DateTimeType
transforms the original field value into a formatted datetime.StringType
casts the value into a string. In addition, it provides an optionalformat
configuration which will be passed to PHP'ssprintf()
function when set.
Check the source files for details on how a specific type works and their available options.
Lastly, you can add your own field type to the type registry. Just refer to the source code of existing types on how to implement one, and refer to services.yml
on how to register the types to the type registry so that you can use it by its name.
Defining exporter fields in a separate class
Just like Symfony's Form component, you can also define exporter fields in a separate class:
<?php
namespace AppBundle\Exporter\Type;
use Sparkson\DataExporterBundle\Exporter\ExporterBuilder;
use Sparkson\DataExporterBundle\Exporter\Type\AbstractType;
class ProfileType extends AbstractType
{
public function buildExporter(ExporterBuilder $builder)
{
$builder
->add('firstName', 'string')
->add('lastName', 'string')
->add('age', 'string');
}
public function getName()
{
return 'user_profile';
}
}
The exporter can then be created by passing the class instance to the factory:
$profileExporter = $factory->createExporter(new \AppBundle\Exporter\Type\ProfileType());
// OR, when the class is registered in the type registry:
$profileExporter = $factory->createExporter('user_profile');
Output adapters
When execute()
is called, the exporter instance iterates the data set one by one, each with the defined column set. But the exporter instance does not handle the writing of the extracted values itself. Such job is delegated to an output adapter.
You can find the following output adapters in this library:
CSVAdapter
, which uses PHP's ownfputcsv()
function to write data.GoogleSpreadsheetAdapter
, which uses asimlqt/php-google-spreadsheet-client to write data to Google Spreadsheet.PHPExcelAdapter
, which utilizes the PHPExcel library to write data.TwigAdapter
, which renders the result using a customizable twig template.
Unlike field types, output adapters are not services, so you need to create them manually. Refer to their source codes for details on constructor arguments and available options. Here shows some brief usage examples:
By default, CSVAdapter
writes data to memory during $exporter->execute()
which can be retrieved with the getResult()
method. In the following example, CSVAdapter
is configured to write the export result to a file instead, and will not keep the result (i.e. it won't read the data back) for further retrieval via getResult()
:
$exporter->setOutputAdapter(new CSVAdapter([
'filename' => __DIR__.'/output.csv', // sets output filename
'keep_result' => false, // do not keep result for getResult()
]));
With TwigAdapter
, you can pass the exported data to a Twig template for further processing. This class requires the Twig_Environment
instance as the first constructor argument.
$twig = $this->get('twig'); // retrieve the Twig_Environment instance from the service container
$exporter->setOutputAdapter(new TwigAdapter($twig, [
'template' => '@AppBundle/exporter/my_exporter_template.html.twig',
]));
Note that the default template located at Resources/view/exporter/template.html.twig
will be used if no template is given. You can use this file to learn writing your own template.
Hint: You can define your own output adapter services. For example, you can define a service for TwigAdapter
that uses your own template. After that the above code example can be simplified as follow:
$exporter->setOutputAdapter($this->get('app.exporter.output_adapter.twig'));
Here is how the service is defined in services.yml
:
app.exporter.output_adapter.twig:
class: Sparkson\DataExporterBundle\Exporter\OutputAdapter\TwigAdapter
arguments:
- @twig
- { template: "@AppBundle/exporter/my_exporter_template.html.twig" }
Misc. notes
Changing column properties
By default, columns are exported in the order they were added. But you may want to modify the column order after the exporter is built. Here's how:
$columnSet = $exporter->getColumns();
$columnSet->getChild('lastName')->setPosition(2);
Note that position
is merely a sorting hint. The library will not modify other columns' positions for uniqueness.
Alternatively, you may re-order the columns via setColumnOrders()
:
$columnSet->setColumnOrders(['age', 'lastName', 'firstName', 'id']);
// Columns re-arranged in the following order: "age", "last name", "first name", "id"
You can also disable a column from the exporter:
$columnSet->getChild('id')->setEnabled(false);
You can even disable columns not specified in setColumnOrders
by passing true
as the second argument:
// Fields in the column set: id, age, firstName, lastName
$columnSet->setColumnOrders(['lastName', 'firstName'], true);
// Columns re-arranged in the following order: "last name", "first name".
// "id" and "age" are disabled.
This is especially useful when you provide an UI for users to choose which columns to export. Instead of adding ifs around the builder's add field statements, just add all of them first and disable the unwanted ones after the exporter is built.
Important: Column properties cannot be changed once $exporter->execute()
is called.
Working with large data sets in Doctrine
To export database records with Doctrine, one would normally write this:
// $em is the Entity Manager
$items = $em->getRepository('AppBundle:EntityToExport')->findAll();
$exporter->setDataSet($items);
But this does not work well for large data sets, as the memory consumption can be very high. Fortunately, Doctrine provides a way to iterate over a large result set. Here is the code slightly modified from Doctrine's documentation:
$q = $em->createQuery('SELECT e FROM AppBundle:EntityToExport e');
$iterableResult = $q->iterate();
foreach ($iterableResult as $row) {
// do stuff with the data in the row, $row[0] is always the object
// detach from Doctrine, so that it can be Garbage-Collected immediately
$em->detach($row[0]);
}
We can adopt this technique using PHP's generator, like so:
$getDataSetIterator = function()
{
$q = $em->createQuery('SELECT e FROM AppBundle:EntityToExport e');
$iterableResult = $q->iterate();
foreach ($iterableResult as $row) {
yield $row[0];
// detach from Doctrine, so that it can be Garbage-Collected immediately
$em->detach($row[0]);
}
};
$exporter->setDataSet($getDataSetIterator());
That's it. For PHP < 5.5 you will need to roll your own iterator wrapper class.