jasny/twig-extensions
Jasny Twig Extensions adds practical filters and functions to Twig for common template tasks, including array manipulation, date/time formatting, and string helpers like truncation and slugs. A lightweight way to reduce custom Twig code in PHP apps.
jasny/twig-extensions is a Twig-only package. It does nothing in a stock Laravel app, because Laravel renders Blade, not Twig. Its value appears in exactly three situations:
rcrowe/twigbridge.TextExtension, DateExtension) without Twig, called from Blade or plain PHP.Install and inspect:
composer require jasny/twig-extensions
composer show jasny/twig-extensions # check the Twig constraint against your Twig version
php -m | grep intl # required for localdate/localdatetime/localtime/slug
Look first at src/DateExtension.php and src/TextExtension.php. The README is short; the tests are the real documentation for exact output (time_ago wording, truncation ellipsis behavior, slug transliteration).
With TwigBridge, register in config/twigbridge.php:
'extensions' => [
Jasny\Twig\DateExtension::class,
Jasny\Twig\TextExtension::class,
Jasny\Twig\ArrayExtension::class,
],
(Newer TwigBridge versions nest this under 'extensions' => ['enabled' => [...]] — check the published config.)
With a bare Twig environment:
$twig = new \Twig\Environment($loader);
$twig->addExtension(new \Jasny\Twig\TextExtension());
$twig->addExtension(new \Jasny\Twig\DateExtension());
First useful template — a blog index:
<a href="/blog/{{ post.title|slug }}">{{ post.title }}</a>
<time>{{ post.published_at|time_ago }}</time>
<p>{{ post.body|strip_tags('<p><a><em>')|truncate(180) }}</p>
Locale-aware dates. localdate, localdatetime, and localtime wrap IntlDateFormatter, so you get ICU formatting rather than date() strings:
{{ order.created_at|localdate('long') }}
{{ order.created_at|localdatetime('medium', app_locale) }}
{{ invoice.due_date|localdate('short', 'nl_NL', 'Europe/Amsterdam') }}
Keep the locale in one place. Pass config('app.locale') (or a user preference) into the Twig environment as a global rather than hardcoding locales in templates.
Relative timestamps for feeds and activity lists.
{{ comment.created_at|time_ago }}
{{ notification.created_at|time_ago }}
This is the highest-value filter in the package — it replaces a hand-rolled Carbon diff block in every template.
Durations from DateInterval. dateinterval renders an interval with pluralization, useful for "reading time" or SLA displays:
{{ ticket.resolution_interval|dateinterval('%d days, %h hours') }}
Text pipelines. The TextExtension filters compose cleanly and are the reason most people install this:
{{ article.body|strip_tags('<p><strong><a>')|truncate_preserve(160) }}
{{ searchResult.body|excerpt(300, searchQuery) }}
{{ user.display_name|slug }}
excerpt is the standout for search-result pages: it centers the snippet on the matched phrase instead of blindly cutting the first 300 characters. Pass the user's query in as the focus argument.
Using the helpers from Blade. If you don't want Twig at all, call the classes directly through a small facade or helper:
// app/Support/Text.php
public static function excerpt(?string $text, int $len, ?string $focus = null): string
{
return (new \Jasny\Twig\TextExtension())->excerpt($text, $len, $focus);
}
{{ \App\Support\Text::excerpt($result->body, 300, request('q')) }}
This is a pragmatic middle ground: you get the excerpt/slug logic without adopting Twig.
Wrap in your own Twig extension. If templates already exist, register a thin extension that delegates to Jasny so you can swap implementations later without touching templates:
class AppTwigExtension extends \Twig\Extension\AbstractExtension
{
public function getFilters(): array
{
return [new \Twig\TwigFilter('excerpt', [new TextExtension(), 'excerpt'])];
}
}
No Blade support, no service provider. There is no Laravel auto-discovery magic beyond PSR-4 autoloading. Without TwigBridge (or your own Twig setup) you will install this and see zero effect.
Stale release. Last tagged release is 2020-10-08, one maintainer, ~107 stars. Verify the Twig version constraint resolves against your Twig 3.x before committing. If it doesn't, the extensions are small enough (a few hundred lines total) to copy into app/Support/Twig/ and own outright.
ext-intl is a hard dependency in practice. localdate/localdatetime/localtime need IntlDateFormatter; slug uses transliteration. Without intl you get class-not-found or silently degraded slugs. Add ext-intl to composer.json explicitly.
Locale strings must be ICU-valid. Use en_US, not en. en often resolves to a root locale and produces odd date output that looks like a bug in the package.
Timezone drift. time_ago and the local* filters read PHP's default timezone. Laravel sets it from config('app.timezone'), but queue workers and CLI commands can diverge. Assert the timezone in AppServiceProvider and in your test bootstrap.
Carbon instances work, but check signatures. Carbon implements DateTimeInterface, so passing Eloquent date casts is fine — but the filters annotate \DateTime. If a filter type-errors, pass $model->created_at->toDateTime().
strip_tags is not a sanitizer. It removes tags it wasn't told to keep but does not strip event handlers or javascript: URLs. Never do {{ userHtml|strip_tags('<a><p>')|raw }} on untrusted input. Run HTMLPurifier first, then strip_tags, then raw.
|raw is still required. Twig autoescapes. strip_tags returning <p>hello</p> renders as visible angle brackets unless you add |raw. The combination strip_tags(...)|raw is the intended pattern — but see the security note above.
Slug inconsistency with Laravel. |slug and Str::slug() use different transliteration rules and separator handling. If route keys are generated with Str::slug but templates link with |slug, you get 404s on accented or non-Latin titles. Pick one function and use it everywhere; I'd lean on Str::slug for anything that becomes a route parameter.
Multibyte and truncation. truncate cuts by character count; truncate_preserve breaks on word boundaries. Test both against CJK and emoji content — neither is guaranteed to be grapheme-safe.
Cacheability. time_ago output changes every minute. Never render it inside a fragment you cache (or cache with a short TTL and re-render client-side). For cached pages, emit an ISO timestamp in <time datetime="..."> and let JS produce the relative string.
ArrayExtension is mostly redundant. Twig core has had filter, map, and reduce since 2.10/3.x. Prefer core and keep this extension out unless you specifically need its older helpers.
Debugging. Clear the Twig cache after changing extension registration — TwigBridge caches compiled templates in storage/framework/views (or its own cache dir). php artisan twig:clean if available; otherwise php artisan view:clear. In Symfony environments, debug:twig lists registered filters, which is the fastest way to confirm the extension actually loaded.
Testing. These filters are pure string transforms — assert on rendered output in feature tests rather than unit-testing the extension directly. That catches escaping and |raw mistakes, which are the actual failure mode.
How can I help you explore Laravel packages today?