alt-three/badger
A Laravel package from Alt Three that simplifies creating and displaying badges in your application. It provides a straightforward API for generating badge images, making it easy to add dynamic status, version, or metadata badges to views and routes.
Badger is a tiny Laravel 5 service provider that turns a couple of strings into an SVG badge (the shields.io-shaped subject | status rectangle). It is a server-side badge factory, not a badge-hosting service — you expose a route, it returns SVG.
Minimal steps
composer require alt-three/badger
Register the provider (Laravel ≤5.4 style; on 5.5+ check vendor/alt-three/badger/composer.json for extra.laravel.providers before duplicating it):
// config/app.php
'providers' => [
// ...
AltThree\Badger\BadgerServiceProvider::class,
],
'aliases' => [
// ...
'Badger' => AltThree\Badger\Facades\Badger::class,
],
Where to look first
Because this package is archived (last release 2018-09-29), read the source before trusting the README: vendor/alt-three/badger/src/. The public surface is deliberately one method on the generator — check Badger.php / the facade docblock for the exact signature in your pinned version. In the 5.x line it is essentially a (subject, status, colour) triple returning an SVG string, with BadgerServiceProvider binding a singleton into the container.
First use case — the one you'll actually ship on day one: a public status badge for your README.
// routes/web.php
use AltThree\Badger\Facades\Badger;
Route::get('badge/{subject}/{status}', function ($subject, $status) {
$svg = Badger::generate($subject, $status, 'green');
return response($svg, 200, [
'Content-Type' => 'image/svg+xml',
'Cache-Control' => 'public, max-age=300',
]);
})->name('badge');
Then embed  in your README. That is 90% of real-world Badger usage.
State-driven badges, not hand-written ones. The value of a badge is that it lies as little as possible. Don't hardcode passing in the route — derive it:
Route::get('badge/tests', function () {
$state = Cache::remember('tests.state', 60, fn () => $this->ci->lastRunState());
$colour = ['passed' => 'brightgreen', 'failed' => 'red', 'running' => 'yellow'][$state] ?? 'lightgrey';
return response(Badger::generate('tests', $state, $colour), 200)
->header('Content-Type', 'image/svg+xml');
});
Keep the state→colour map in config/badger.php (publishable with php artisan vendor:publish --provider="AltThree\Badger\BadgerServiceProvider" if the package ships one) so designers can retune without a deploy.
Cache aggressively, generate cheaply. SVG generation is string concatenation plus some escaping — not free if a README gets hammered. Two layers:
Cache-Control: public, max-age=300 (README renderers and GitHub's camo proxy respect it).Cache::remember("badge:{$subject}:{$status}:{$colour}", 300, fn () => Badger::generate(...)).Cache on the inputs, not the output object, so you can swap engines later.
Wrap it. Given the package is archived, do not sprinkle Badger::generate() across controllers. Bind your own thin contract and delegate:
// app/Badges/BadgeRenderer.php
class BadgeRenderer
{
public function render(string $subject, string $status, string $colour = 'lightgrey'): string
{
return Badger::generate($subject, $status, $colour);
}
}
When Badger finally breaks on a modern PHP/Laravel, you change one method to call pugx/badge-poser, a hand-rolled SVG template, or redirect to shields.io — and nothing else moves.
Parameterised badges via a single route. One route with {subject}/{status} beats five named routes; constrain them with ->where(['subject' => '[a-z0-9\-]{1,20}', 'status' => '[a-zA-Z0-9\-\. ]{1,20}']) to keep junk out of your SVG and your cache keys.
Testing. Assert the contract, not the pixels:
$response = $this->get('/badge/build/passing');
$response->assertOk();
$this->assertSame('image/svg+xml', $response->headers->get('Content-Type'));
$this->assertStringContainsString('passing', $response->getContent());
$this->assertStringContainsString('<svg', $response->getContent());
If you're on a Laravel version with snapshot testing, freeze one SVG per colour and diff it — that's how you notice a dependency bump silently changing the badge geometry.
Integration tips. Badges are usually consumed cross-origin by an image tag, so no CORS work is needed; but if you expose a JSON endpoint that returns badge definitions alongside, add barryvdh/laravel-cors. If the route is public and uncached, put it behind throttle:60,1 — an unbounded SVG generator on the internet is a free CPU donation.
It's abandoned — treat that as the headline risk. illuminate/support is pinned to the 5.x era, so composer require will hard-fail on Laravel 8/9/10 and on PHP 8 unless you fight the constraint solver. --ignore-platform-reqs will get you an install that then fatals at runtime. The realistic options are: (a) fork and bump constraints yourself, (b) vendor the ~200 lines of SVG logic into app/, or (c) use pugx/badge-poser / hit shields.io's /badge/{subject}-{status}-{colour} endpoint and cache it. For a new project, (b) or (c).
Escape user input or you have SVG injection. Badge subjects and statuses often come from query params or model fields. If the generator interpolates raw text into <text> nodes, <script> in a status string is persisted XSS served from your origin with an image content type. Sanitise before calling (strip_tags, whitelist regex) and add X-Content-Type-Options: nosniff to the response so browsers don't sniff an SVG into something executable.
Content-Type is not optional. Returning a bare string from a route makes Laravel send text/html; the <img> tag renders a broken icon and you'll spend twenty minutes blaming the generator. Always response($svg)->header('Content-Type', 'image/svg+xml').
Facade alias is a 5.4-era requirement. If you're on 5.5+ and the alias "just works", auto-discovery did it — don't add it twice. If Class 'Badger' not found, run composer dump-autoload and confirm the provider is actually in config/app.php (and that config:cache isn't serving a stale providers list — php artisan config:clear after editing).
Debug in Tinker, not in the browser. php artisan tinker → app(AltThree\Badger\Badger::class) or Badger::generate('a','b','red') isolates generator failures from routing/HTTP failures. If the container can't resolve it, the provider binding is the problem; if it resolves but the SVG is malformed, it's the underlying renderer.
Colours fail silently. Most badge generators accept a small named palette (brightgreen, green, yellowgreen, yellow, orange, red, blue, lightgrey) plus #rrggbb. Pass gray instead of grey and you may get a silently defaulted badge with no exception. Validate against your own whitelist in BadgeRenderer and log the fallback — otherwise your "all green" dashboard will one day be all grey and nobody will notice for a week.
Fonts and rendering. SVG badges rely on a font stack (Verdana, Geneva, DejaVu Sans, sans-serif). They look correct in browsers and on GitHub, but if you ever rasterise (PNG for Slack, email) you need rsvg-convert or ImageMagick with the right fonts installed, and the widths will differ from the browser. Don't promise pixel-identical output across surfaces.
Extension point is the container, not the config. There's no rich plugin API. If you need custom layout, a logo, or a different geometry, rebind the generator in a service provider ($this->app->singleton(Badger::class, MyBadger::class)) or — better — keep the wrapper from the previous section and bypass the package entirely for that case. A 50-star archived package is a dependency you rent, not one you renovate.
How can I help you explore Laravel packages today?