stinger-soft/twig-extensions
Twig Extensions for PHP adds handy filters, functions and tags to the Twig templating engine, making it easier to write expressive templates in Laravel and other PHP projects. A lightweight package that plugs into existing Twig setups with minimal configuration.
1. Install the package
composer require stinger-soft/twig-extensions
The package is a plain Twig extension library (Apache-2.0), not a Laravel service provider — it ships filters/functions/tests as a Twig ExtensionInterface implementation. There is nothing to publish and no migrations.
2. Confirm you actually have Twig in Laravel
Twig is not a first-party Laravel view engine. In practice you are using rcrowe/twigbridge, which wires a Twig\Environment into the container:
composer require rcrowe/twigbridge
php artisan vendor:publish --provider="TwigBridge\ServiceProvider"
3. Register the extension
Open config/twigbridge.php and add the extension class to the extensions array:
'extensions' => [
// ...
StingerSoft\TwigExtensions\Extension::class, // verify FQCN in composer.json "autoload"
],
4. Where to look first
vendor/stinger-soft/twig-extensions/composer.json — real namespace, PHP and twig/twig version constraints (this is the compatibility gate).vendor/stinger-soft/twig-extensions/src/ — the class implementing getFilters(), getFunctions(), getTests(). That file is the documentation; with 0 stars and 0 dependents there is no tutorial ecosystem to lean on.5. First use case
The realistic first win is a Blade-to-Twig migration where you need a handful of string/array helpers without writing your own extension class:
{{ article.title|some_string_filter }}
{% if value is some_test %}...{% endif %}
Render a single throwaway template to prove registration before touching production views:
Twig::renderString('{{ "x"|your_filter }}'); // via TwigBridge facade
Register once, globally, then get out of the way. Twig extensions are cheap; the compiled-template cache is what matters. Put the class in config/twigbridge.php under extensions and stop thinking about it. Per-controller registration is possible by resolving Twig\Environment and calling addExtension() early in the request, but it fragments behaviour and defeats template caching across requests.
Use it standalone when you are not in Blade land. The extension is framework-agnostic, which is its main selling point:
$twig = new \Twig\Environment($loader);
$twig->addExtension(new StingerSoft\TwigExtensions\Extension());
This is the pattern for mail renderers, PDF/queue workers, or a artisan command that renders templates outside the HTTP stack.
Compose, don't duplicate. If the package gives you generic string/array filters, build your domain filters on top in an app-owned extension rather than copying logic into templates:
class AppTwigExtension extends \Twig\Extension\AbstractExtension
{
public function getFilters(): array
{
return [
new \Twig\TwigFilter('money', fn ($v) => number_format($v / 100, 2)),
];
}
}
Register both in twigbridge.php; order matters only for name collisions (last registered wins).
Testing templates. Unit-test rendered output with the extension attached, not the extension internals:
$twig = new \Twig\Environment(new \Twig\ArrayLoader([
't' => '{{ value|your_filter }}',
]));
$twig->addExtension(new StingerSoft\TwigExtensions\Extension());
$this->assertSame('expected', $twig->render('t', ['value' => 'input']));
This doubles as your compatibility harness — it will fail loudly on the day you upgrade Twig.
Integration tips
storage/framework/views (or php artisan view:clear) after changing extension registration; Twig compiles to PHP and will happily serve stale bytecode.queue:work after config changes so the environment rebuilds.Vendor risk is the headline. Zero stars, zero dependents, and a generic name mean no community backstop. Before adopting: read every filter body, check the twig/twig constraint in composer.json, and pin an exact version ("stinger-soft/twig-extensions": "1.0.3") or vendor the source into app/Support/Twig/. If it is abandoned mid-project, forking is cheap — it is a single extension class plus helpers.
Twig 2 → 3 is the likely breakage point. getFilters()/getFunctions() returning bare callables and Twig_SimpleFilter were removed in Twig 3 in favour of TwigFilter/TwigFunction objects. A low-traffic package may predate that. If it does not install cleanly, add twig/twig to your root composer.json with the version the package supports and check nothing else forces Twig 3.
Escaping and is_safe. Any filter that returns markup must be declared safe in the extension, otherwise {{ html|your_filter }} is escaped and you get <div>. Conversely, |raw in the template is a code smell that usually means the extension forgot ['is_safe' => ['html']]. Audit this deliberately — it is the most common security bug in third-party Twig filters.
Null and type coercion. These helper libraries frequently call mb_strtolower/array_* on the raw value. Twig resolves missing variables to null, so {{ undefined_var|your_filter }} can throw. Guard with {% if x is defined and x is not null %} or wrap the filter in your own null-safe layer.
Name collisions. Generic filter names (slug, truncate, markdown) already exist in Twig core, TwigBridge's own extensions, or twig/extra-bundle. Registration silently overrides. Enumerate $twig->getFilters() once and diff it against what you expect before shipping.
Debugging inside the extension. Twig swallows little. Add {{ dump() }} only with the debug extension enabled (twigbridge has a debug config flag) and APP_DEBUG=true; otherwise use dd() inside the filter callable, which still works because filters are plain PHP invoked at render time.
Extension points are shallow by design. There is no config file and no service provider, so "configuration" means subclassing the extension and overriding getFilters()/getFunctions() to remove or replace entries, or registering a competing filter after it. Treat the class as a sealed unit and prefer your own extension for anything project-specific — that keeps the upgrade path to a fork or a replacement trivial.
How can I help you explore Laravel packages today?