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 View Laravel Package

slim/twig-view

Twig-View integrates the Twig templating engine with Slim Framework, enabling .twig template rendering in Slim 4 applications. It offers middleware, a configurable Twig environment, extensions, and flexible view rendering for clean, maintainable PHP apps.

View on GitHub
Deep Wiki
Context7

Getting Started

Install the bridge; it pulls in twig/twig 3.x — 3.11.1 minimum as of slim/twig-view 3.4.1, which is a security-only release that raises the Twig floor to patch GHSA-6j75-5wfj-gh66 (a Twig sandbox/security fix):

composer require slim/twig-view

If you already have the package, the upgrade is:

composer update slim/twig-view twig/twig --with-dependencies
composer show twig/twig   # confirm >= 3.11.1

There are no API, signature, or behavioural changes in 3.4.1 for the bridge itself — Twig::create(), render(), fetch(), and the url_for/full_url_for/is_current_url/current_url/get_uri functions are untouched. Treat this as a dependency-hygiene release, not a migration.

The code you’ll actually read is small: src/Twig.php (the renderer), src/TwigMiddleware.php (route-aware wiring), src/TwigRuntimeExtension.php (the url_for/full_url_for/is_current_url/current_url/get_uri functions). Everything else is stock Twig 3, so the Twig docs are your template reference.

Minimal wiring with Slim 4’s default container:

use DI\Container;
use Slim\Factory\AppFactory;
use Slim\Views\Twig;
use Slim\Views\TwigMiddleware;

$container = new Container();
AppFactory::setContainer($container);
$app = AppFactory::create();

$twig = Twig::create(__DIR__ . '/../templates', [
    'cache'            => __DIR__ . '/../var/cache/twig',
    'debug'            => true,
    'strict_variables' => true,
]);

$container->set(Twig::class, $twig);
$app->add(TwigMiddleware::createFromContainer($app)); // needs Twig::class in the container

$app->get('/hello/{name}', function ($request, $response, array $args) {
    return $this->get(Twig::class)->render($response, 'hello.twig', [
        'name' => $args['name'],
    ]);
})->setName('hello');

$app->run();
{# templates/hello.twig #}
<!doctype html>
<p>Hello {{ name }}</p>
<a href="{{ url_for('hello', {'name': name}) }}">permalink</a>

Two methods are your day-to-day interface:

  • render($response, 'page.twig', $data) — writes the rendered output to the response body and returns the PSR-7 response.
  • fetch('page.twig', $data) — returns the rendered string. Use this for emails, PDFs, log messages, or JSON error bodies (fetch() then json_encode()).

Your first useful swap-out: replace inline echo/heredoc HTML in a route with render(), then replace hard-coded links with url_for() so routes stay named rather than duplicated as strings.

Implementation Patterns

Layouts and partials map cleanly to Blade muscle memory.

{# layout.twig #}
<html><body>{% block content %}{% endblock %}</body></html>

{# page.twig #}
{% extends 'layout.twig' %}
{% block content %}
  {% include 'partials/nav.twig' with {'user': user} only %}
{% endblock %}

Use only deliberately: unlike Blade’s @include, Twig passes the parent context by default, which is convenient but makes templates implicitly coupled to every caller.

Macros are the closest thing to anonymous Blade components.

{% macro input(name, value = '', type = 'text') %}
  <input type="{{ type }}" name="{{ name }}" value="{{ value }}">
{% endmacro %}
{% import 'forms.twig' as forms %}
{{ forms.input('email', user.email) }}

There are no class-based components with typed props and slots; if you need that, use {% embed %} or include with explicit variables.

Register application helpers as Twig functions/filters, usually grouped in one extension class:

$env = $twig->getEnvironment();

$env->addFunction(new \Twig\TwigFunction('asset', fn (string $p) => '/assets/' . ltrim($p, '/')));

$env->addFilter(new \Twig\TwigFilter('money', fn (int $cents) => number_format($cents / 100, 2)));

For helpers that need request-scoped services (auth user, CSRF token, translator), prefer a Twig runtime class plus a RuntimeLoaderInterface. It resolves lazily per call and avoids stuffing request data into globals. This is the same extension/runtime split Symfony uses; it feels heavier than Blade directives but pays off in long-running workers.

CSRF, flash, auth, translation are bring-your-own. Slim has no equivalent of @csrf, old(), session(), __(), @auth, or @can. The standard pattern is:

$env->addFunction(new \Twig\TwigFunction('csrf_field', function () use ($csrf) {
    return sprintf(
        '<input type="hidden" name="%s" value="%s">',
        $csrf->getTokenName(),
        $csrf->getTokenValue()
    );
}, ['is_safe' => ['html']]));

Use is_safe => ['html'] instead of making templates call |raw; it keeps the trust boundary in PHP where you can see it. slim/flash pairs naturally: add a middleware that puts the flash messages into the environment (or a runtime object) before the handler runs, then {% for msg in flash.getMessages('success') %}.

Rendering errors and 404s. Either render inside the route:

return $twig->render($response->withStatus(404), 'errors/404.twig', ['path' => $request->getUri()->getPath()]);

or register a Twig-backed HTML error renderer with Slim’s ErrorHandler via registerErrorRenderer('text/html', MyTwigErrorRenderer::class). The second is what you want if you dislike Slim’s default error page.

Container and constructor injection. Closure routes are bound to the container, so $this->get(Twig::class) works. For class actions ($app->get('/', HomeAction::class)), constructor-inject Twig and let PHP-DI autowire it.

Middleware ordering. TwigMiddleware binds the Twig runtime extension to the current request URI and gives you route-aware functions. If is_current_url() / current_url() look wrong, that’s an ordering problem: Twig middleware is executing before routing has populated the route context. Slim’s middleware stack is LIFO (last added = outermost = first executed), so adjust the add() order and verify with a template that calls is_current_url() on a named route.

Testing. You don’t need a Slim app to test templates: Twig::create($fixtures, ['cache' => false]) + fetch() covers syntax, escaping, and partial inclusion. Use nyholm/psr7 or Slim’s response factory when you need to assert on render() output.

Gotchas and Tips

  • 3.4.1 is a security bump, so treat the lock file as the deliverable. The release changes nothing in src/ that you write against; it only constrains twig/twig to >=3.11.1 to address GHSA-6j75-5wfj-gh66. The practical failure mode is not a runtime error but a Composer resolution error: if something else in your tree pins Twig lower, composer update slim/twig-view will refuse. Diagnose with composer why twig/twig and composer why-not twig/twig 3.11.1, then widen or bump the conflicting constraint (symfony/twig-bridge, twig/extra-bundle, and Twig-adjacent extensions are the usual suspects).
  • Exposure depends on whether you run the Twig sandbox. Twig advisories of this class matter most when you render templates you don’t fully control (SandboxExtension with a policy, user-editable templates, CMS snippets). If every template is a file you wrote and reviewed, the risk is lower — but there is no reason to sit on the old version, and the bump costs nothing.
  • Audit in CI, not by hand. Add composer audit --locked (or --format=json for parsing) to the pipeline so the next Twig advisory surfaces as a red build rather than a release-notes read. roave/security-advisories as a dev dependency is the belt-and-braces alternative if you want the resolver to refuse vulnerable versions outright.
  • Don’t pin the bridge and float Twig independently without checking. Locking slim/twig-view:3.4.0 to keep a twig/twig version another package demands re-opens the advisory. If a transitive dependency genuinely blocks 3.11.1, record it as a tracked risk with an owner and a deadline rather than silently downgrading.
  • Twig is not Blade. {{-- --}} is {# #}; {!! !!} is |raw; @extends/@section are {% extends %}/{% block %}; @include is {% include %}; filter is apply. There is no @php block by default, no @csrf, no @error, no @vite.
  • Property access can call methods. {{ user.name }} tries array key, public property, name(), getName(), isName(). That means {{ user.delete }} or a getter with side effects is a real risk. Keep domain objects boring, or pass explicit view models/DTOs.
  • Set strict_variables => true in development. Laravel/Blade quietly tolerates missing variables in many cases; Twig’s default is also lenient, which hides typos until production. Turn it on locally, and consider leaving it off in production to avoid 500s on optional data.
  • dump() needs two things: 'debug' => true in the environment options and $twig->getEnvironment()->addExtension(new \Twig\Extension\DebugExtension()). It is not enabled by the Slim bridge automatically.
  • Cache directory permissions. The cache path must be writable by the PHP user, same class of problem as storage/framework/views. In development 'cache' => false is fine; in production, compile once and keep the cache across requests. Clear it on deploy (the equivalent of php artisan view:clear) — symlink-based releases can otherwise leave compiled templates pointing at old absolute paths. Also clear it after this upgrade: compiled templates are keyed to the Twig version that produced them, and a stale cache makes it look like the bump didn’t take.
  • No automatic template extension. render(..., 'index') won’t find index.twig unless you build a loader convention. Pass full filenames ('index.twig'), or register a custom loader/resolver.
  • render() appends to the body. Calling it twice on the same response duplicates output. If you need to compose content, use fetch() and write once, or create a fresh response.
  • url_for outside HTTP. CLI commands, queue jobs, and tests that render email templates have no Slim request, so url_for() will fail or produce unusable relative URLs. Configure a separate Twig environment for those contexts and use absolute URLs from config rather than router helpers.
  • Long-running runtimes (RoadRunner, Swoole, ReactPHP). The Twig environment and its globals persist between requests. Anything request-scoped added with addGlobal() leaks into the next request. Use middleware to set/restore globals, or move request data into runtime extensions that read from the current request.
  • Escape contexts beyond HTML. Autoescape defaults to html; in JS or attribute contexts use |e('js') / |e('html_attr'). This is the same discipline as Blade’s e() but Twig makes the context strategy explicit.
  • Extension points worth knowing: $twig->getEnvironment()->getLoader()->addPath($path, 'admin') gives you @admin/dashboard.twig namespaces; addGlobal() for genuinely static config; ProfilerExtension + Twig\Profiler\Profile if you want per-template timings; and a custom ErrorRendererInterface if you want Twig-rendered 404/500 pages instead of Slim’s defaults.
  • Route names are the contract. url_for('profile', {'name': name}) throws when the route name is missing or the parameter set is incomplete; that’s usually a better failure than a silently broken hard-coded URL, but it means renaming routes is a template-breaking change.
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.
tequilarapido/trackit
gregsphotography/site-builder-filament
cabaoctavio/laravel-query-log
gregsphotography/site-builder-components
io-digital/clickatell
snowsoft/laravel-admin
oxygen/auth
relicsoftware/barakah-payment-gateway
tobya/work-with-composer
equipc/laravel-email-validator
equipc/laravel-email-database-log
adedsec/nova-translated-labels
wazza/laravel-db-encryption
hokny/laravel-generator
vonsogt/backup
jengo/queues
jeremykenedy/laravel-observability
coleus/frame
thtg88/journalism
filagora/filagora-ai-forums