recranet/twig-spreadsheet-bundle
Symfony bundle adding Twig functions and filters for working with spreadsheets, so templates can read, render, and export spreadsheet data without extra PHP glue. Lightweight integration for generating Excel or CSV output directly from Twig views.
Version 2.5 is a maintenance-focused release with a hard platform floor, so the first thing to do is confirm your app can satisfy it before touching any Twig code.
Requirements as of 2.5:
| Dependency | Constraint |
|---|---|
| PHP | ^8.2 (8.1 dropped) |
| Twig | ^3.21 (was lower) |
| Symfony components | ^7.4 || ^8.0 (6.4 and 7.0–7.3 dropped) |
Minimal steps:
composer why symfony/console
composer why symfony/http-foundation
Laravel 10 pins Symfony 6.4, so it will not resolve against 2.5. Laravel 11/12 will, provided the resolved Symfony minor is ≥ 7.4.composer require "vendor/package:^2.5" -W
php artisan vendor:publish --tag=twigbridge --force # only if you want the config refreshed
php artisan view:clear && php artisan cache:clear
debug, strict_variables, cache), the extensions array, and template paths..twig file into resources/views and render it exactly like a Blade view — return view('welcome.twig', ['name' => $user->name]);. If that works, the bridge is wired correctly and the rest is configuration.Where to look first when something breaks: the config file, then storage/framework/views (compiled templates), then storage/logs/laravel.log for Twig\Error\* entries.
Blade/Twig coexistence. The bridge registers .twig with the view finder, so both engines run side by side. The practical pattern is Twig for page-level templates and Blade for anything that depends on Blade-only features (components, @livewire, @vite directives). Don't try to make Twig a drop-in Blade replacement.
Registering extensions is the main extension surface:
// config/twigbridge.php
'extensions' => [
App\Twig\MoneyExtension::class,
App\Twig\FeatureFlagExtension::class,
],
Extensions are resolved from the container, so constructor injection of services works — this is where you put formatting helpers, feature-flag functions, and domain-specific filters.
Laravel helpers inside Twig. The shipped Laravel extension exposes the usual suspects as functions/filters/tests: route(), asset(), csrf_field(), trans()/__(), old(), config(), plus auth/guest/env tests:
{% if auth %}
<a href="{{ route('profile') }}">{{ auth.user.name }}</a>
{% endif %}
Sharing data globally. Use the bridge's share mechanism or a service provider for values every template needs (app name, locale, feature flags) rather than threading them through every controller.
Composing templates. Use standard Twig inheritance ({% extends %}, {% block %}, {% include %}) and lean on the shared data for layout chrome. If you need a Blade partial inside Twig, render it once in a controller or a dedicated function rather than reaching across engines from the template.
Testing. Assert against the rendered string ($response->assertSee(...)) rather than Twig internals. When you need the environment itself — e.g. to verify an extension is registered — resolve the bridge and inspect getEnvironment()->getExtension(...). Clear the compiled cache in test setup if you alter extension config.
CI matrix. Pin the intersection explicitly in your build: PHP 8.2/8.3/8.4 × Twig 3.21+ × the Symfony minors your Laravel versions resolve to. A composer update --prefer-lowest job will surface constraint breaks early.
-W is required for the bump. Without composer require -W, Composer may leave a stale Twig 3.x minor locked and then complain about an unsatisfiable constraint that looks unrelated to what you changed.Twig\Error\SyntaxError at render time, not at boot. Run your template test suite, not just your unit tests.php artisan view:clear is the fix for "my new filter doesn't exist" nine times out of ten.Htmlable handling, so |raw is genuinely dangerous — restrict it to output you constructed yourself. Likewise, {{ }} inside a Vue/Alpine block will be consumed by Twig; use {% verbatim %} around front-end templates.csrf_field() exists as a function, but <form method="POST"> inside Twig does not get Blade's implicit hidden field. Use {% include %} of a shared form helper or call the function in every form.strict_variables in local only. Turning it on in production converts missing keys into 500s; leaving it off in development hides typos. Bind it to app.debug, and remember twig.debug controls the dump function separately.storage/framework/views so view:clear and deploy tooling handle both engines uniformly.TwigExtension classes, the container-resolved extensions array, and the bridge's share/global API. If Symfony 8 support surfaces runtime incompatibilities, report them upstream — this release shows the maintainer tracks Symfony majors, so waiting a patch is usually cheaper than vendoring.How can I help you explore Laravel packages today?