twig/string-extra
Twig extension integrating Symfony String, adding u, slug, singular, and plural filters for Unicode-aware text manipulation, URL-friendly slugs, and simple word inflection directly in your templates.
composer require twig/string-extra
You almost certainly already have symfony/string — it is a transitive dependency of symfony/console, which Laravel ships with. Verify before adding it explicitly:
composer show symfony/string
Do not pin a fresh ^6 or ^7 unless you know your Laravel version’s transitive constraint; let the lock file resolve it.
u, slug, singular, plural. That is the entire public surface. If you are looking for truncate, sanitize, or titlecase, they are not here — those come from UnicodeString methods accessed via the u filter.u filter is your entry point into UnicodeString, and its method list (truncate, title, camel, snake, folded, trimSuffix, replace, …) is the actual API you will use day-to-day.src/StringExtension.php) — worth reading once. It is ~80 lines and tells you the exact argument order for each filter, which the README does not.If your Laravel app uses Twig (via twigbridge / laravel-twigbridge), register the extension and use it immediately:
// config/twigbridge.php or config/twig.php
'extensions' => [
\Twig\Extra\String\StringExtension::class,
],
<a href="/blog/{{ post.title|slug }}">{{ post.title }}</a>
{# "Laravel 11 & You!" → /blog/laravel-11-you #}
If you are on Blade (the default and most common case), you will not use the extension’s Twig filters at all — you will call Symfony\Component\String\UnicodeString directly through a Blade directive or a Str macro. That is the realistic Laravel starting point, and it is covered in the next section.
Register once in a service provider (AppServiceProvider::boot() or a dedicated TemplatingServiceProvider):
use Illuminate\Support\Facades\Blade;
use Symfony\Component\String\UnicodeString;
Blade::directive('slug', function ($expression) {
return "<?php echo \\Symfony\\Component\\String\\UnicodeString::from({$expression})->slug(); ?>";
});
<a href="/blog/{{ slug($post->title) }}">{{ $post->title }}</a>
This is the lowest-friction way to give Blade parity with the Twig slug filter. Do the same for singular, plural, and any u-chained method you use regularly.
Str macro (Blade + PHP consistency)If your team already writes Str::of() in controllers, register matching macros so template and backend code look the same:
use Illuminate\Support\Str;
Str::macro('plural', fn ($value, $locale = 'en') => (string) \Symfony\Component\String\UnicodeString::from($value)->pluralize($locale));
Str::macro('singular', fn ($value, $locale = 'en') => (string) \Symfony\Component\String\UnicodeString::from($value)->singularize($locale));
{{ Str::plural($model->type) }} {{-- "categories" --}}
Advantage: works in both Blade and PHP with no directive parsing quirks.
Once StringExtension is registered, all four filters are available. The high-value use is chaining on u, because UnicodeString has no Twig-native equivalent:
{{ article.body|u.truncate(140, '…') }}
{{ user.name|u.title }}
{{ input|u.folded }} {# accent-insensitive comparison #}
{{ 'foo_bar'|u.camel }} {# "fooBar" #}
Note the ergonomics: u returns a UnicodeString, Twig then dispatches the trailing method and auto-casts to string on output.
{{ page.title|slug('-', app.request.locale) }}
{{ count ~ ' ' ~ noun|plural(app.request.locale) }}
The slug filter wraps AsciiSlugger::slug(), so it shares Laravel’s Str::slug() behaviour for ASCII, but gives you per-call locale, which Laravel’s helper does not expose cleanly. This is the single strongest reason to prefer the filter over Str::slug() in a multilingual site.
Pick a side: either the model writes a slug column on save, or the template derives it on render. Do not do both. Template-side slugging is appropriate for transient URLs (preview links, share URLs on unsaved records); persistent, indexed URLs should be generated on save in PHP so they can be unique-constrained and changed deliberately.
Do not scatter UnicodeString::from(...) calls across dozens of directives. Create a small helper class (App\Support\Text) with static methods (Text::slug(), Text::initials()), and have Blade directives / Str macros / Twig extensions delegate to it. This is the seam you test.
The package does not give you Blade filters. There is no {{ $x | slug }} in Blade. You are always bridging via a directive, macro, or helper. Plan for a TemplatingServiceProvider rather than one-off Blade::directive calls sprinkled around.
u returns an object, not a string. If you pipe it into something expecting a string before the terminal method, Twig may attempt __toString() at the wrong time. Chain only UnicodeString methods after u — ByteString methods like slice() from CodePointString differ subtly from AbstractUnicodeString::slice().
UnicodeString::from(null) throws. Real data hits null. Guard at the call site:
Blade::directive('slug', fn ($e) => "<?php echo \\Symfony\\Component\\String\\UnicodeString::from((string) {$e})->slug(); ?>");
Note the explicit (string) cast — it makes the directive null-safe.
Blade::directive receives the raw expression, including quotes. slug($post->title) comes in as '$post->title'. {$expression} interpolates it back into the compiled PHP. If the expression itself contains <?php, compilation breaks; keep directive arguments simple variables/accessors rather than complex expressions.
View caching hides directive bugs. After registering a new directive, run php artisan view:clear before judging whether it worked. A stale compiled view is the #1 false alarm.
slug() separator and locale argument order is slug(string $string, string $separator = '-', ?string $locale = null). Passing a locale where you meant a separator (|slug('de')) silently produces de-separated slugs instead of a locale-specific slug. Double-check signatures in the source.
AsciiSlugger customisation is not exposed by this extension. If you need custom transliteration rules (e.g., currency symbols, CJK fallbacks), you must write your own StringExtension subclass or a separate service that injects a configured AsciiSlugger. Do not expect the filter to accept a slugger instance.
plural() on already-plural irregulars can be surprising. UnicodeString::pluralize() follows the Inflector rules; "data" → "datas" and "sheep" → "sheeps" happen. For user-facing copy, prefer explicit translation strings over inflection, or test with your actual vocabulary.
Non-ASCII and RTL. Test explicitly with Café, Ærøskøbing, 東京, and Arabic input. Symfony handles these well, but Blade directive outputs need to be wrapped in {{ }} (auto-escaped) — the directive itself must not escape, or you get double-encoding.
Watch for duplicate work in loops. @foreach ($posts as $post) {{ slug($post->title) }} runs the slugger N times per request. If your pages are list-heavy, precompute slugs into a view model or @php block. The slugger caches internally but the UnicodeString allocation does not.
Version alignment. twig/string-extra (v3.x) pairs with Twig 3 and symfony/string v6+. If your Laravel version still resolves symfony/string ^5.4, you will get Twig filter registration errors at boot. Run composer why symfony/string once, and keep twig/string-extra on a matching minor.
Extension points to remember: (a) register your own Twig\Extension\AbstractExtension subclasses alongside this one if you need extra filters; (b) Str::macro for a PHP-first API; (c) Blade::directive for zero-dependency template sugar. Pick the one that matches where your team already lives.
The u filter is the real workhorse, not slug. Most teams install this package expecting slug, then discover the value is in {{ x|u.title }}, {{ x|u.truncate(80) }}, and {{ x|u.camel }} — because those are the operations that previously leaked custom helpers or JS into templates. Document the u chaining pattern for the team, or the package looks thinner than it is.
How can I help you explore Laravel packages today?