madeitbelgium/spintax
Process spintax templates in Laravel with madeitbelgium/spintax. Expand {a|b|c} syntax into random variations for emails, copy, and SEO content, with a simple API that integrates cleanly into any PHP or Laravel app.
Install
composer require madeitbelgium/spintax
Laravel wiring — the package ships a service provider with package auto-discovery, so on Laravel 5.5+ there is nothing to register. The facade is aliased as Spintax. On older apps, add to config/app.php:
'providers' => [
MadeITBelgium\Spintax\SpintaxServiceProvider::class,
],
'aliases' => [
'Spintax' => MadeITBelgium\Spintax\Facades\Spintax::class,
],
First use case — pick one variant out of a template:
use MadeITBelgium\Spintax\Facades\Spintax;
Spintax::parse('{Hello|Hi|Hey} there, {friend|mate}');
// => "Hi there, mate" (re-randomised on every call)
Where to look first
src/Spintax.php — the actual parser. Read this before anything else; it is small, and the public method name (parse() / spin()) has drifted across madeITBelgium packages, so confirm it there rather than trusting a blog post.src/SpintaxServiceProvider.php — confirms the facade alias and whether a config file is publishable (php artisan vendor:publish --provider="MadeITBelgium\Spintax\SpintaxServiceProvider").Framework-agnostic use
$spintax = new \MadeITBelgium\Spintax\Spintax();
echo $spintax->parse('{Red|Green|Blue}');
Use this form inside Artisan commands, queue workers, or plain PHP scripts where you don't want the container involved.
Wrap it behind your own class. The package is a single static-ish call; injecting the concrete package class all over your app makes it painful to swap later. Bind a thin wrapper:
// app/Support/TextSpinner.php
namespace App\Support;
class TextSpinner
{
public function spin(string $template): string
{
return app('spintax')->parse($template);
}
}
// AppServiceProvider::register()
$this->app->bind('spintax', fn () => new \MadeITBelgium\Spintax\Spintax());
Now tests can swap the binding for a deterministic stub.
Mail and notifications. Spintax shines in subject lines, where the variation is invisible to the user but affects open rates:
public function envelope(): Envelope
{
return new Envelope(
subject: Spintax::parse('{Your|Quick} {invoice|receipt} is {ready|available}'),
);
}
Generate the subject inside envelope() and not in the constructor — queued mails are serialised and rebuilt, so computing it at send time keeps each delivery distinct.
Blade helper. Register a directive once and use it in views:
Blade::directive('spin', fn ($expr) => "<?php echo e(app('spintax')->parse($expr)); ?>");
@spin('{Welcome|Good to see you}, {{ $user->name }}')
Note the escaped {{ }} — Blade compiles its own braces first, so any spintax containing {{ needs care (see Gotchas).
Seeding test data. This is the highest-value day-to-day use: varied but plausible fixture text without a faker library.
User::factory()->create(['bio' => Spintax::parse(
'{Software|Backend|Full-stack} developer who {loves|enjoys} {PHP|Laravel|clean code}.'
)]);
Unique-variant batching. When you need N distinct outputs (subject line A/B lists, sitemap descriptions), loop with a guard:
$seen = [];
$attempts = 0;
while (count($seen) < $count && $attempts++ < $count * 10) {
$seen[Spintax::parse($template)] = true;
}
if (count($seen) < $count) {
// The template has fewer combinations than you asked for — log it.
}
Bound the loop. A template with three binary choices yields eight outcomes; asking for fifty will spin forever otherwise.
Config-driven templates. Keep spintax strings in config/ or the database rather than inline in PHP, so marketers can edit copy without a deploy, and you can preview the variant space in a Nova/Filament panel.
Every call re-randomises — there is no seed. You cannot reproduce the same output twice, and there is no parse($template, $seed) equivalent. Tests must mock the facade, and anything requiring a stable string (URLs, cache keys, stored snapshots) must be generated once and persisted, never regenerated on read.
Spintax::shouldReceive('parse')->once()->andReturn('Hello there');
It expands one variant, not all of them. If you need the full permutation set for a preview UI, this package won't give it to you — you'll have to write a recursive expander yourself. Budget for that early; teams routinely assume "spintax library" means "enumerate".
Nested braces are supported, but depth isn't documented. {Hello|Hi {there|friend}} works, but deeply nested or unbalanced braces ({a|{b|c}) can silently drop text or consume more than intended. If the templates are user-supplied, validate balanced braces before parsing, and cap nesting depth.
Literal braces are a real problem. Spintax has no escape character, and Blade's {{ }} collides directly with it. If your copy contains CSS, JSON, or Blade expressions, either parse before handing the string to Blade, or use @verbatim blocks and a placeholder token that you swap in afterwards.
Empty options. Check what {Yes|} produces — depending on the regex version, you may get an empty string or a stray pipe. Filter templates on save: reject || and leading/trailing pipes.
Whitespace is preserved verbatim. {Hello | Hi} yields "Hello " or " Hi" with the trailing/leading space baked in. Always write {Hello|Hi} with no padding, or trim the final result if your template formatting demands readable source.
PHP version drift. Last release was July 2022, before PHP 8.3/8.4 became common. The parser is regex-based and framework-agnostic, so it almost certainly runs fine, but if you hit deprecation notices (dynamic properties, ${var} interpolation), check composer.json constraints and consider pinning the package with a dev-master requirement if a fix lands upstream. With zero dependents listed, don't expect upstream urgency.
Extension points are minimal. There is no driver interface — if you need weighted randomness, or deterministic output by seed, you cannot configure it. Fork or wrap: subclass the parser and override the random-selection step, or pre-process weighted syntax into plain spintax by repeating options ({A|A|A|B} gives 75/25).
Multibyte content. The regex operates on bytes; accented and CJK text inside options generally survives, but if you see truncated characters, add the u modifier in your fork.
Debugging tip. When output looks wrong, dump the template with var_dump() first — most "parser bugs" turn out to be Blade eating the braces, an env var interpolating, or a . in the string being concatenated before it reaches the parser.
How can I help you explore Laravel packages today?