Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Twig Spreadsheet Bundle Laravel Package

mewesk/twig-spreadsheet-bundle

A Symfony bundle that brings Twig templating to spreadsheet generation, letting you design XLSX, CSV, and other spreadsheet files with familiar Twig syntax instead of low-level APIs. Build clean, reusable templates for reports and exports, keep layout logic separate from PHP code, and render dynamic...

View on GitHub
Deep Wiki
Context7

Getting Started

First, the reality check. mewesk/twig-spreadsheet-bundle is a Symfony bundle, not a Laravel package. It has no service provider, no Blade view compiler, no config/ publish tag. It wires PhpSpreadsheet into Symfony's DI container and Twig's tag/extension system. If your day job is Laravel, you are not installing this — you are reading it as a reference architecture and reimplementing the three good ideas in it (Twig/Blade template → PhpSpreadsheet object; named styles; format-agnostic writers) with phpoffice/phpspreadsheet directly. That is almost certainly the right call: the bundle was archived in July 2018, has zero dependent packages, and pins you to a Symfony 3/4-era dependency graph. The 1.1 release (bugfixes, two new Twig functions, richer CSV writer config) is the end of the line — there is no 1.2 coming.

If you genuinely maintain a legacy Symfony 3/4 app, the minimal path is:

composer require mewesk/twig-spreadsheet-bundle:^1.1
  1. Register it: AppKernel::registerBundles() (Symfony 3) or config/bundles.php (Symfony 4). There is no Flex recipe, so nothing happens automatically.
  2. Add the small config block — at minimum the temp directory (the bundle writes scratch files), the image cache directory (separate from the temp dir — see Gotchas), and the default writer.
  3. Confirm compatibility before writing a line of code:
composer show mewesk/twig-spreadsheet-bundle   # inspect the require block
composer why-not mewesk/twig-spreadsheet-bundle:1.1

Bundles from this era frequently require the monolithic symfony/symfony package plus phpoffice/phpspreadsheet:^1.0, which will conflict with a modern app installing individual components. Resolve this before you invest.

Where to look first (in order):

  • README.md — usage examples for the factory service and Twig tags.
  • src/Twig/ — the extension and node classes. This is the ground truth for tag names and accepted attributes; they drifted across the 2018 releases. Since 1.1, also check src/Twig/Extension (or wherever the functions are registered) for xlscellindex / xlsrowindex.
  • src/DependencyInjection/Configuration.php — every knob the bundle exposes, including writer registration and the per-writer CSV options added in 1.1.
  • Resources/config/services.yml — the real service IDs (the factory, the extension, the writers).

First use case — a controller action that renders a report template and streams an XLSX download:

public function export(SpreadsheetFactory $factory): StreamedResponse
{
    $spreadsheet = $factory->createSpreadsheet('spreadsheet/invoices.xlsx.twig', [
        'invoices' => $this->invoiceRepository->findForExport(),
    ]);

    return new StreamedResponse(
        fn () => (new Xlsx($spreadsheet))->save('php://output'),
        200,
        [
            'Content-Type'        => 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
            'Content-Disposition' => 'attachment; filename="invoices.xlsx"',
        ]
    );
}

The template is a normal Twig file wrapped in the bundle's spreadsheet tag, mirroring PhpSpreadsheet's worksheet → row → cell model:

{% spreadsheet %}
  {% worksheet 'Invoices' %}
    {% row %}
      {% cell 'Number' %}{% cell 'Customer' %}{% cell 'Total' %}
    {% endrow %}
    {% for invoice in invoices %}
      {% row %}
        {% cell invoice.number %}{% cell invoice.customer %}{% cell invoice.total %}
      {% endrow %}
    {% endfor %}
  {% endworksheet %}
{% endspreadsheet %}

(Verify the exact tag/attribute names against src/Twig/Node for your installed version — this is the single most common stumbling block.)

Implementation Patterns

Keep controllers thin; put generation in a service. Wrap the factory in your own SpreadsheetGenerator with a method like generate(string $template, array $data, string $format): string. Controllers then only decide what and which format; the generator owns temp files, writer selection, and error handling. This also gives you one place to swap out the bundle later.

One template per report, format in the filename. templates/spreadsheet/invoices.xlsx.twig, templates/spreadsheet/invoices.csv.twig when the layouts genuinely diverge (they often do — CSV has no styling, so headers and totals need different treatment). Derive the writer from the extension at the edge of your app, not inside the template.

Templates are presentation only — shape your data first. Twig loops over Doctrine proxies or Eloquent models will lazy-load per cell and destroy performance. Map to plain arrays or a small DTO in the repository/service before handing data to the factory. This is the same discipline as Maatwebsite\LaravelExcel's FromQuery/WithMapping pattern, and it makes the templates trivially testable.

Use xlscellindex / xlsrowindex for dynamic footers (new in 1.1). Before this release, a "Total" row placed after a variable-length loop had no way to know its own coordinates for formula references. Now you can do:

{% row %}
  {% cell 'Total' %}
  {% cell '=SUM(C2:C' ~ (xlsrowindex() - 1) ~ ')' %}
{% endrow %}

Both functions return the current zero-based (verify) position, so they are only meaningful inside a row/cell block, and they reflect the position at evaluation time — buffer the value into a Twig variable if you reference it after emitting more cells.

Reuse via Twig includes and macros. Header rows, footer totals, and company branding blocks belong in spreadsheet/_header.xlsx.twig and get pulled in with {% include %}. The bundle does not invent its own inheritance model — it uses Twig's, so {% extends %} works.

Centralise styles in config, not in templates. Define named formats once (currency, date, header) and reference them from cells. Inline style arrays scattered across 30 report templates are unmaintainable; a config-level style block is one edit.

Push CSV writer quirks into config, not code (expanded in 1.1). 1.1 added writer-level CSV configuration (value separator, enclosure, line ending, BOM). Define a named CSV writer per locale/output target in the bundle config — csv_de with ;, csv_utf8_bom with BOM enabled — and select by name in your generator. This removes the writer->setDelimiter() scattergun from your service layer.

Stream, don't buffer. $writer->save('php://output') inside a StreamedResponse avoids materialising the whole file in a string first. Call ob_end_clean() / disable output buffering if you see corrupted bytes prepended — a stray BOM or whitespace from a service file will silently break the archive.

Queue the big ones. PhpSpreadsheet holds the entire workbook in memory. For exports over a few thousand rows, generate in a Symfony MessageHandler / Laravel job, write to disk or S3, and email or link to the result. Keep the synchronous controller path for small "quick download" reports only.

Test by reading the file back. Do not snapshot the template output. Load the generated file with IOFactory::load() in your test and assert specific cell values and number formats:

$sheet = IOFactory::load($path)->getActiveSheet();
$this->assertSame('INV-0001', $sheet->getCell('A2')->getValue());
$this->assertSame('#,##0.00', $sheet->getStyle('C2')->getNumberFormat()->getFormatCode());

The Laravel translation of all this is: phpoffice/phpspreadsheet in a service class, a Blade (or TwigBridge) template for layout, and Excel::download() / a queued ShouldQueue export if you'd rather not write the writer plumbing yourself. Same architecture, zero Symfony coupling.

Gotchas and Tips

Archived and unmaintained. Last release 2018-07-29, 22 stars, zero dependents. 1.1 fixed three real bugs and added two useful functions — which is also the strongest evidence that nobody is left to ship 1.2. There will be no fix for a PhpSpreadsheet 2.x/3.x breaking change or a Symfony 6/7 deprecation. Budget for a fork or a rewrite; treat it as a starting point, not a foundation.

Dependency conflicts are the likely first failure. The 2018 require block almost certainly targets symfony/symfony and phpoffice/phpspreadsheet:^1.0. Modern Flex apps and Laravel both install granular packages. Run composer why-not before promising anyone a timeline.

Images have their own cache directory (fixed in 1.1, but still a config you must set). Issue #13 was "incorrect cache directory for images" — the bundle writes extracted/embedded images to a cache path that was previously derived wrongly. 1.1 corrects the derivation, which means if you had worked around it with a hardcoded path in a Symfony 3 app, that workaround may now point at the wrong place. Check Configuration.php for the image cache key and make it explicit: %kernel.cache_dir%/twig_spreadsheet/images. A read-only container filesystem surfaces this as a vague Twig/writer exception, not a permissions error.

pre_calculate_formulas — decide deliberately. Issue #15 was a bug in this flag. Formula cells only carry a cached result if PhpSpreadsheet calculates them, which costs time on large sheets; if you disable it, consumers that read cached values (pandas, Google Sheets import, some LibreOffice paths) may see 0 or blank until Excel recalculates. Set it explicitly in config rather than relying on the default, and test the file in the actual consumer, not just Excel.

xlsmergestyles now overrides non-array values (behaviour change). Previously it merged non-array style values with each other instead of overriding, so {'border': 'thin'} followed by {'border': 'thick'} could yield surprising results. In 1.1 the later value wins. If you depended on the old merge semantics — or papered over them with duplicated style names — audit those templates. The fix is correct, but it is a silent output change, not an error.

Memory is the real ceiling, not the bundle. The bundle adds templating, not streaming; the "improved internal index handling / performance" in 1.1 is micro-level. PhpSpreadsheet builds the full workbook in memory; a 50k-row XLSX will OOM regardless. Mitigations: raise memory_limit for the process, chunk your data with a cell cache, or switch to a true streaming writer (openspout/openspout) for large CSV/XLSX exports.

Formula injection is your security problem, not the bundle's. Any cell value beginning with =, +, -, or @ from user input will be interpreted as a formula when the file opens in Excel. Twig's HTML autoescaping is irrelevant inside an XLSX — it does nothing for you here. Sanitise those prefixes or use explicit string typing (setCellValueExplicit(..., DataType::TYPE_STRING)). Note that 1.1's new xls*index functions encourage emitting formulas — keep the = prefix on trusted, server-built strings only.

Whitespace is content. In HTML, a newline between tags is invisible. In a spreadsheet template, it can become part of a cell value or shift your layout. Use {%- -%} trimming aggressively around cell tags and keep the template dense at the top level.

Dates need conversion, not stringification. Passing a DateTime into a cell gives you a formatted string that Excel can't sort or compute on. Convert with \PhpOffice\PhpSpreadsheet\Shared\Date::PHPToExcel($dt) and apply a date number format so the value stays numeric underneath.

CSV needs a BOM for Windows Excel. Configure it on the writer (now expressible in bundle config) rather than post-processing the stream — accented characters will otherwise mojibake for anyone opening the file by double-clicking. Set the delimiter explicitly for locales that expect ; (DE/FR). If you still see sep=, issues, remember the BOM and the separator must be set on the same named writer.

Debugging: exceptions bubble up as Twig render errors with template line numbers, which is genuinely helpful. Use {{ dump() }} with Twig debug enabled to inspect the data you're passing in, and catch around the factory call so you can log the template name and parameter keys — a missing variable in a spreadsheet template is otherwise silent (empty cell, no error) because Twig's strict_variables is often off. This applies double to xlscellindex/xlsrowindex used outside a row/cell context.

Template name from user input is an injection vector. Never let a request parameter choose the template path. Twig sandboxing is not enabled by default and templates are server-side code — keep the mapping from "report type" to template name a hardcoded whitelist.

Extension points worth knowing: custom writers are registered in the bundle config (implement PhpOffice\PhpSpreadsheet\Writer\IWriter), named styles are added alongside them, and the Twig extension service can be decorated or overridden if you need a house-specific tag or want to expose your own position/format helpers next to xlscellindex. These are the seams to use if you fork — they're cleaner than patching the node classes.

MIT licensed, so forking is legally frictionless. Given the maintenance status, fork-and-own is the realistic long-term option, and the codebase is small enough that reading src/Twig and src/DependencyInjection end-to-end takes an afternoon. Pin ^1.1 when you fork so your fixes sit on top of the CSV/index improvements rather than the buggy 1.0 line.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
3mdikram/worldmoneywords
snowsoft/config
itmosoft/laravel-subscriptions
larva/laravel-ai-qwen
diephp/sequences
users-au/socialite-provider
twidpay/twid-logger
mrnewport/laravel-plaid
mrnewport/laravel-repo
mrnewport/laravel-docsign
mrnewport/laravel-stow
mrnewport/laravel-flow
mrnewport/laravel-priceable
mrnewport/laravel-nestedterms
fredmeng/laravel-apidoc-generator
itstructure/laravel-multilingual-tools
pdustdar/elasticsearch
markupai/php-sdk
rlacerda83/lumem-microservice-shipment
uni-method/symfony-lib