fungio/dataexporter-bundle
A Symfony bundle for exporting data to CSV, Excel, and other formats. Define exporters, map columns, and generate downloadable files from arrays or Doctrine queries through a configurable, extensible workflow—ideal for reports and admin panels.
Install and register
composer require fungio/dataexporter-bundle
// config/bundles.php
return [
// ...
Fungio\DataExporterBundle\FungioDataExporterBundle::class => ['all' => true],
];
With Symfony Flex the bundle registration is usually automatic; without it you must add the entry by hand. There is no recipe for a 4-star package, so assume manual registration.
Where to look first (in this order)
Resources/config/services.yml (or config/services.yaml) in the bundle — this is the source of truth for service IDs. Do not trust README examples, since they drift between tags; the YAML does not.DependencyInjection/Configuration.php — tells you whether the bundle exposes a fungio_data_exporter: config tree at all. If it's absent, everything is convention-based and there is nothing to configure.Exporter/ (or Export/) directory — the ExporterInterface is what you should type-hint against, not concrete classes.First use case: CSV download from a controller
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
class ReportController extends AbstractController
{
public function export(ExporterInterface $exporter): Response
{
$rows = [
['id', 'name', 'total'],
[1, 'Widget', '19.99'],
[2, 'Gadget', '24.50'],
];
$content = $exporter->export($rows);
return new Response($content, 200, [
'Content-Type' => 'text/csv; charset=UTF-8',
'Content-Disposition' => 'attachment; filename="report.csv"',
]);
}
}
If autowiring fails, bind explicitly in config/services.yaml:
services:
Fungio\DataExporterBundle\Exporter\ExporterInterface:
alias: fungio.data_exporter.csv # confirm exact ID via `debug:container`
Confirm the ID before you write the alias:
bin/console debug:container | grep -i fungio
Format switching at runtime. The bundle's value proposition is one interface, five formats. Wrap the individual exporters in your own small registry rather than if/else-ing service IDs in every controller:
final class ExportRegistry
{
/** @param array<string, ExporterInterface> $exporters */
public function __construct(private array $exporters) {}
public function forFormat(string $format): ExporterInterface
{
return $this->exporters[$format]
?? throw new \InvalidArgumentException("Unsupported format: $format");
}
}
services:
App\Export\ExportRegistry:
arguments:
$exporters:
csv: '@fungio.data_exporter.csv'
json: '@fungio.data_exporter.json'
xml: '@fungio.data_exporter.xml'
html: '@fungio.data_exporter.html'
xls: '@fungio.data_exporter.xls'
Use explicit keys instead of !tagged_iterator unless the bundle actually tags its services — tags are the first thing that goes missing in a small, low-traffic package.
Feeding it Doctrine results. Exporters almost certainly take a flat array of rows. For scalar exports use getArrayResult() on a Query, not getResult(), so you avoid hydrating entities you're about to flatten:
$rows = $this->getDoctrine()->getRepository(Order::class)
->createQueryBuilder('o')
->select('o.id', 'o.reference', 'o.total')
->where('o.createdAt >= :from')
->setParameter('from', $from)
->getQuery()
->getArrayResult();
Reusable export action. Keep the controller thin — put header generation (content type, filename, charset) in a single service or trait so all five formats share one code path and one set of headers.
Testing. Assert on Content-Type and Content-Disposition headers, and snapshot the body for a fixed fixture. This is the cheapest way to catch a bundle upgrade silently changing its output format (e.g. delimiter, escaping, XML root element).
Verify the API before trusting the README. With 4 stars and 0 dependents there is essentially no community proof-reading of the docs. The method signature may be export(array $data), export(array $data, array $options = []), or a fluent builder. Read the interface file in vendor/ first; it takes two minutes and saves an afternoon.
XLS is probably not a real XLS. Small exporters usually emit an HTML table with an .xls extension, or an XML Spreadsheet 2003 file. Excel will open it with a warning; LibreOffice and phpspreadsheet readers will not. If downstream users need real binary files, budget for PhpSpreadsheet instead.
Memory blows up on big exports. A string-returning export() builds the entire document in memory. That's fine to a few thousand rows and painful past ~50k. For large datasets, batch with ->toIterable() and stream with StreamedResponse + fputcsv, or move to league/csv's Writer and skip this package for the CSV path specifically.
CSV formula injection. Any cell beginning with =, +, -, or @ becomes executable in Excel/Sheets. If you export user-supplied strings, prefix with a single quote or a tab before handing rows to the exporter. This is a your-code problem, not a bundle problem — but nobody warns you.
UTF-8 in Excel. Excel misreads UTF-8 CSV without a BOM. Prepending "\xEF\xBB\xBF" to the CSV output fixes it and breaks nothing else.
XML escaping and shape. Expect no schema, no configurable root element, and no CDATA handling. Any &, <, or > in your data should be escaped by the exporter — verify it with a fixture containing &, otherwise you ship invalid XML to a consumer's parser.
Symfony version ceiling. Check composer.json for the symfony/* constraint. A bundle this small is often pinned to ^4.4|^5.0; on Symfony 6/7 the AbstractBundle/Extension signatures may break, and the fix is a fork. Decide up front whether you're comfortable maintaining that fork.
Swap-ability is the real design decision. Because maintenance is uncertain, type-hint against your own App\Export\ExporterInterface and adapt the bundle behind it. Then replacing the package with league/csv + symfony/serializer later is a one-file change, not a refactor of every controller.
Debugging. bin/console debug:container --show-private | grep fungio will surface services that autowiring hides; debug:container fungio.data_exporter.csv --show-arguments confirms which concrete class backs the ID when aliases point in circles.
Extension point. If the bundle tags its exporters, adding a format means implementing ExporterInterface and registering your class with that same tag — cleaner than duplicating format-switching logic. If there is no tag, skip the abstraction and register your exporter in the explicit registry map shown above.
How can I help you explore Laravel packages today?