shadesoft/user-agent-parser-bundle
A PHP bundle that parses user-agent strings to identify browser, operating system, device type, and bot traffic, exposing the results through a simple service you can inject wherever request analytics, logging, or content adaptation is needed.
Before writing a single line of integration code, note the headline: this package is archived and abandoned, carries 0 stars and 0 dependents, and is a Symfony bundle, not a Laravel package. It exists as a thin DI wrapper around the underlying ShadeSoft/UserAgentParser library, whose only job is to turn a User-Agent header into a browser name/version and an OS name/version. Treat the bundle as documentation of how to wire up the library, not as something you install into a Laravel app.
Where to look first, in order:
composer.json of the repo — check the require block for PHP and symfony/* version constraints. Packages of this vintage typically pin PHP ^5.5/^7.0 and Symfony 2/3, which will refuse to install on a modern Laravel app (PHP 8.1+, Symfony 6/7 components via illuminate/*). If Composer blocks it, stop here.DependencyInjection/ and Resources/config/services.yml — this is where the actual service ID for the parser is defined. That service ID is the only thing of value you can lift.shadesoft/user-agent-parser library — it holds the real API (parse(), getBrowser(), getOS() style methods) and, critically, the stale UA regex/database that determines output quality.First use case: request-level enrichment for analytics — capture browser and os alongside every page view, login event, or support ticket so you can answer "which browsers do our users actually run?" without a third-party analytics vendor.
Minimal viable path in Laravel (bypassing the bundle entirely, which is the only sane approach given its state):
composer require shadesoft/user-agent-parser # the library, not the bundle
// app/Providers/UserAgentServiceProvider.php
public function register(): void
{
$this->app->singleton(UserAgentParser::class, fn () => new UserAgentParser());
}
$ua = app(UserAgentParser::class);
$parsed = $ua->parse(request()->userAgent() ?? '');
// $parsed['browser'], $parsed['os'] — names from a stale lookup table
If the library itself won't install either, that is your answer: do not use this package, and jump to the "Gotchas" migration note.
1. Container binding + injectable service. Never call the parser statically or instantiate it inline in controllers. Bind the parser as a singleton (construction is not free — it loads regex tables) and inject it:
class TrackPageView
{
public function __construct(private UserAgentParser $ua) {}
public function handle(Request $request, Closure $next)
{
$parsed = $this->ua->parse($request->userAgent() ?? '');
$request->attributes->set('ua', [
'browser' => $parsed['browser'] ?? 'unknown',
'os' => $parsed['os'] ?? 'unknown',
]);
return $next($request);
}
}
2. Cache by UA hash — this is the single most important pattern. A given browser sends an identical UA string on every request; re-parsing per request burns CPU on regex matching. Hash and memoize:
$result = Cache::remember(
'ua:'.md5($request->userAgent() ?? ''),
now()->addDay(),
fn () => $this->ua->parse($request->userAgent() ?? '')
);
3. Persist normalized columns, not raw blobs. Model it as browser_name, browser_version, os_name, os_version rather than a JSON column — you will want GROUP BY browser_name in analytics queries and JSON grouping is painful across MySQL/Postgres/SQLite.
4. Blade/display helper. For support tooling, a tiny presenter that turns the parsed array into a badge is enough:
// "Chrome 120 on Windows 10"
{{ $ua['browser'] }} on {{ $ua['os'] }}
5. Fallback-everywhere wrapper. The parser returns sparse arrays on unrecognized input and the library is unmaintained, so wrap it once and never call it raw:
public function summarize(?string $header): array
{
try {
$r = $this->parser->parse($header ?? '');
} catch (\Throwable) {
$r = [];
}
return [
'browser' => $r['browser'] ?? 'unknown',
'os' => $r['os'] ?? 'unknown',
];
}
6. Test with a fixtures table. UA parsing is pure-function-shaped, so make it table-driven and assert against a known-good set:
yield 'chrome-on-windows' => [
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0 Safari/537.36',
['browser' => 'Chrome', 'os' => 'Windows'],
];
7. If you genuinely must run the Symfony bundle, it can only live inside a Symfony kernel — you'd register it in config/bundles.php (Symfony 4+) or AppKernel::registerBundles(), not in Laravel's bootstrap/app.php. There is no Laravel auto-discovery, no facade, no config publish. Bridging it means manually instantiating the bundle's DI extension, at which point installing the underlying library directly is strictly less work.
It is abandoned. This is the dominant gotcha. No security patches, no PHP 8.2/8.3 compatibility fixes, no response to CVEs in transitive Symfony components. Any audit will flag it.
UA databases go stale in months, not years. New Chrome/Edge/Safari major versions and new OS releases (Windows 11, iOS 17/18, Android 15) will fall through to "unknown" or be misattributed. An abandoned parser is guaranteed to be wrong about current traffic within a release cycle.
Symfony bundle ≠ Laravel package. No service provider, no config/ publish, no facade. Every Laravel integration requires hand-written glue; budget for that and question whether it's worth it.
UA strings are attacker-controlled and spoofable. Never use the output for access control, feature gating, bot blocking, or security decisions. It is analytics/display metadata only. A one-line curl -A spoofs it trivially.
Bot misclassification. This era of parser tends to lump crawlers and headless browsers into desktop-browser buckets. If bot traffic matters, cross-check with a separate signal (robots.txt fetch, reverse DNS, X-Requested-With, rate behavior) rather than the parser.
Performance. Regex-heavy parsing on every request is measurable. Always pair with the cache-by-UA-hash pattern; consider parsing lazily (only when the analytics event actually fires) rather than in global middleware.
Config quirks. If you do read the bundle, its config namespace is defined by the Configuration class in DependencyInjection/ — expect a bundle-specific root key (something like shade_soft_user_agent_parser) with at most a service-ID override. There is nothing meaningful to tune, which also means no extension points worth preserving; the only override is swapping the parser service for a different implementation.
Missing User-Agent header. API clients, health checks, and some CLI tools omit it entirely — the parser will throw or return empty. Default to unknown before it ever reaches the parser.
Migration path (recommended). Replace this package outright:
jenssegers/agent — lightweight, Laravel-native, wraps mobiledetect; good for mobile/desktop/bot flags.matomo/device-detector — actively maintained, large maintained device database; the right choice for real analytics.browscap/browscap-php — heavier, but the most accurate for browser/OS version detail, with regularly updated data.All three are drop-in replacements for the summarize() wrapper above, so write that wrapper once and you can swap the backend without touching call sites.
How can I help you explore Laravel packages today?