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 Service Bundle Laravel Package

zenstruck/twig-service-bundle

Twig Service Bundle adds a service() function to Twig so templates can fetch Symfony services directly by ID, e.g. {{ service('my_service') }}, plus service_exists() for safe checks. It keeps controllers thin and avoids writing custom Twig extensions just to expose a service.

View on GitHub
Deep Wiki
Context7

Getting Started

Install with Composer and register the bundle (Symfony Flex will do this automatically):

composer require zenstruck/twig-service-bundle
// config/bundles.php
return [
    Zenstruck\TwigServiceBundle\ZenstruckTwigServiceBundle::class => ['all' => true],
];

Where to look first

  1. The package README — the whole API surface is small; the config reference and the "static method" example are the two things you need.
  2. config/packages/zenstruck_twig_service.yaml — this is where you register everything.
  3. The bundle's own test fixtures (in vendor/zenstruck/twig-service-bundle/tests) if the README leaves a syntax question unanswered.

First use case

Take a static helper you already have and expose it as a Twig function, replacing a full AbstractExtension subclass:

namespace App\Util;

final class Money
{
    public static function format(int $cents, string $currency = 'USD'): string
    {
        return number_format($cents / 100, 2).' '.$currency;
    }
}
# config/packages/zenstruck_twig_service.yaml
zenstruck_twig_service:
    functions:
        - [App\Util\Money, 'format']   # -> {{ format(1999) }}
<p>Total: {{ format(order.totalCents) }}</p>

That is the entire value proposition: you skip writing a dedicated Twig extension class, registering it as a service, and tagging it twig.extension.

Implementation Patterns

Expose static helpers as functions/filters/tests

The common day-to-day pattern is a small static utility class (Money, Text, Dates, Url) with pure methods, wired once in YAML:

zenstruck_twig_service:
    filters:
        - [App\Util\Text, 'slugify']       # {{ post.title|slugify }}
        - [App\Util\Text, 'excerpt']       # {{ body|excerpt(120) }}
    functions:
        - [App\Util\Money, 'format']
    tests:
        - [App\Util\Flags, 'isFeatureOn']  # {% if isFeatureOn('new_checkout') %}

Keep these classes pure and side-effect free. If a helper needs a dependency (clock, HTTP client, repository), it belongs in a service, not a static class.

Expose service methods for stateful or DI-backed work

For anything that needs injected dependencies — a Markdown renderer, a feature-flag client, a formatter reading config — register the service, and the bundle bridges it into Twig:

zenstruck_twig_service:
    services:
        App\Service\MarkdownRenderer:
            - render
            - renderInline
{{ page.body|markdown_rendered }}

This is the pattern that actually saves wiring: the alternative is a MarkdownTwigExtension that injects MarkdownRenderer and forwards one method. Repeat that five times and the boilerplate adds up; this bundle collapses it to YAML.

Prefer a thin presentation facade

Rather than exposing a fat domain service directly, expose a small facade whose method signatures are Twig-friendly (scalars in, strings/arrays out):

final class TemplatePresenter
{
    public function __construct(private MarkdownRenderer $md, private FeatureFlags $flags) {}

    public function markdown(string $text): string
    {
        return $this->md->render($text);
    }

    public function feature(string $name): bool
    {
        return $this->flags->isEnabled($name);
    }
}

This gives you one registration point, keeps template-facing APIs narrow, and leaves room to change internals without touching Twig.

Replacing ad-hoc Twig extensions

Audit your src/Twig/ directory. Any extension that is basically "one method, one-line forwarding" is a candidate to delete and replace with a config block. Keep real extensions (ones with getFilters(), getFunctions(), custom node visitors, or runtime classes) as they are — the bundle is not a replacement for those, it is a shortcut for trivial callables.

Testing

Because the callable is just a class or a service, you can unit-test the underlying method normally. For template-level verification, render the template in a functional test and assert on output:

$this->assertStringContainsString('$19.99', $this->renderTemplate('{{ format(1999) }}'));

Integration tips

  • Group registrations by feature (money.yaml, markdown.yaml) using imports if your config file grows.
  • Use the same class for a function and a filter only when the semantics match; otherwise duplicate names get confusing fast.
  • If a method needs an alias (render conflicts with another exposed name), name the PHP method explicitly for Twig (renderMarkdown()) instead of relying on config-level aliasing.

Gotchas and Tips

  • Verify the exact config schema before copy-pasting. The bundle is small (13 stars); the README is authoritative and the syntax for aliases/named args has shifted between releases. Run bin/console config:dump-reference zenstruck_twig_service to see the live schema for your installed version.
  • Private services. Register services by class or service id, not by grabbing them from the container manually. If the bundle resolves services lazily through a locator, private visibility is fine and preferred; if you see "service not found" in a compiler pass, check that the class is autowired and not excluded, rather than flipping it public.
  • Name collisions. Twig's built-ins (date, number_format, upper) and any other extensions share one namespace. A duplicate registration throws at compile time; if a name feels generic, prefix it (app_format).
  • Exposed = public API. Every method you register is callable from every template, including ones edited by less-trusted users (e.g. CMS templates). Don't expose services with side effects (Mailer::send, repositories with delete), and don't expose anything taking non-scalar objects — Twig will not have a way to construct them.
  • Null and type safety. Twig passes whatever the template gives you. A method typed string $x will throw TypeError on null. Either default your parameters string $x = '' or guard inside the method.
  • Debugging. bin/console debug:twig lists all registered functions, filters, and tests — if yours is missing, the config file isn't being imported, the bundle isn't registered, or the cache is stale.
  • Cache in prod. Twig's compiled template cache plus Symfony's container cache mean config changes need bin/console cache:clear to show up. In dev this is automatic; in a Docker/CI setup, remember to warm the cache.
  • Performance. This is convenience, not optimization. The registration compiles down to ordinary Twig callables; there is no measurable overhead versus a handwritten extension. Do not expect lazy-loading magic for expensive services — if the service is heavy, keep the method cheap or move the expensive work out of the template.
  • Static methods and testability. Pure static helpers are fine, but they bypass DI. If you find yourself adding self:: dependencies or reading $_ENV inside a static helper, promote it to a service and register it under services: instead.
  • The "should I just write an extension?" test. If you have exactly one callable and no plans to add more, write a 15-line Twig extension — the dependency isn't worth it. This bundle pays off at roughly 3+ trivial callables, or when you want non-PHP-config-savvy developers to expose existing methods without touching PHP.
  • Adoption risk. Low star count means fewer eyes on edge cases and slower fixes for schema changes. Pin the version in composer.json, and if the bundle ever becomes a blocker, the migration path is mechanical: replace each config entry with a forwarding method on one consolidated AppTwigExtension.
  • Laravel context. This is a Symfony bundle and won't drop into Laravel natively. If you're in a Laravel codebase, the equivalent pattern is a single Blade directive or a twigbridge extension; use this package only if the project runs the Symfony DI container.
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.
coleus/frame
thtg88/journalism
filagora/filagora-ai-forums
rlacerda83/query-parser
socialdept/atp-orm
cboxdk/laravel-assistant-client
socialdept/atp-resolver
socialdept/tenant-domains
tautvydasr/blade-class-names
socialdept/color-scales
maherelgamil/controller
coleus/widgets
coleus/table
coleus/users
coleus/music
coleus/health
coleus/calendar
coleus/notes
coleus/support
coleus/settings