jbi/browscap-bundle
Symfony bundle integrating the browscap/browscap-php library to detect browser capabilities from user-agent strings. Adds service configuration and caching for fast, accurate browser, platform, and crawler detection in Symfony applications.
Read this first: the package itself is not for you. jbi/browscap-bundle is a Symfony2 bundle (not Symfony 3/4+, not Laravel), it is archived, has 3 stars and zero dependents. There is no Laravel service provider, no config file, no facade. composer require jbi/browscap-bundle into a Laravel app will pull Symfony 2.x framework components that conflict with modern Laravel's symfony/* constraints. Do not install it.
What you actually want is the library it wraps: browscap/browscap-php (the Browser Capabilities Project PHP library). That is the same engine you'd be using through the bundle, minus the dead Symfony glue. Treat the bundle as documentation of intent — "detect browser, platform, device type, and crawler status from a User-Agent string, backed by an aggressively cached lookup table" — and reimplement that intent in Laravel.
Minimal path:
composer require browscap/browscap-php
mkdir -p storage/app/browscap # persistent cache dir — see Gotchas
Then bind the library as a singleton in a service provider, create an artisan command that refreshes the data, schedule it, and call it from one place (middleware or a job) before wiring it into anything else.
Where to look first, in order:
vendor/browscap/browscap-php/src/Browscap.php and BrowscapUpdater.php — the 3.x and 4.x APIs differ (3.x returns stdClass from getBrowser(), 4.x returns a Browser object with getBrowser(), getVersion(), getPlatform(), isMobile(), isCrawler()). Check which one you got before writing glue code.DependencyInjection/Configuration.php on GitHub — it tells you what knobs the original author considered essential (update_method, cache path, the browscap service id). That's your config schema in Laravel.browscap.ini is large and refreshed regularly; you'll be downloading and converting it.First use case: log enrichment. Add a Monolog processor that appends browser/platform/crawler flags to every log line. It's read-only, zero user-visible risk, and it tells you immediately whether detection quality is good enough for your traffic before you gate any behavior on it.
// app/Logging/BrowscapProcessor.php
public function __invoke(array $record): array
{
$ua = substr((string) request()->userAgent(), 0, 512);
if ($ua === '') {
return $record;
}
$b = app(Browscap::class)->getBrowser($ua);
$record['extra']['ua'] = [
'browser' => $b->getBrowser(),
'version' => $b->getVersion(),
'platform' => $b->getPlatform(),
'mobile' => $b->isMobile(),
'crawler' => $b->isCrawler(),
];
return $record;
}
Singleton + decorator, never construct per request. Parsing or rehydrating the ini per request is a guaranteed latency disaster. Bind once, wrap with a request-level memo, and put a Laravel-cache decorator in front of the expensive lookup keyed by a hash of the UA:
class CachedBrowscap
{
public function __construct(private Browscap $browscap) {}
public function parse(?string $ua): array
{
$ua = substr((string) $ua, 0, 512);
$key = 'browscap:'.sha1($ua);
return Cache::store('redis')->remember($key, now()->addDay(), function () use ($ua) {
$b = $this->browscap->getBrowser($ua);
return [
'browser' => $b->getBrowser(),
'version' => $b->getVersion(),
'platform' => $b->getPlatform(),
'mobile' => $b->isMobile(),
'crawler' => $b->isCrawler(),
];
});
}
}
Cache the normalised array, not the library object — it's smaller, serialisable, and decouples you from API changes on upgrade.
Middleware for request context. Resolve once in middleware, stash it on the request or a scoped container binding, so controllers, Blade views, and the logger all read the same result:
public function handle($request, Closure $next)
{
$request->attributes->set('ua_info', app(CachedBrowscap::class)->parse($request->userAgent()));
return $next($request);
}
Then $request->attributes->get('ua_info')['crawler'] anywhere downstream. Avoid a global helper that re-resolves per call site.
Bot/crawler handling — pick your poison deliberately. Three distinct patterns, and people conflate them constantly:
abort(403) for isCrawler() in middleware. Fine for scrapers, catastrophic if you catch Googlebot/Search Console. Maintain an allowlist of verified crawlers by reverse-DNS, not by UA string.Analytics/job enrichment. If you queue analytics events, parse the UA at dispatch time or in the worker — but capture the raw UA string in the payload, never the parsed result. Parsed data ages badly when your browscap tables are stale for three months; raw UA strings can be re-parsed later.
Update as a scheduled command. This is the piece the Symfony bundle gave you for free (a console command plus update_method config for local vs remote data). In Laravel:
// app/Console/Commands/BrowscapUpdate.php
public function handle(BrowscapUpdater $updater): int
{
$updater->update(); // downloads + converts into the cache dir
return self::SUCCESS;
}
// routes/console.php
Schedule::command('browscap:update')->weekly()->withoutOverlapping();
Run it on deployment as a post-deploy step too, so the first request after a release isn't paying for a cold cache.
Where the bundle's extension points map in Laravel: its cache service becomes your PSR-16 adapter bound in the provider; its update_method becomes a config flag choosing between update() (remote) and updateFromLocalFile() (shipping a vendored ini in the image); its service id browscap becomes a container binding plus, if you like, a facade alias.
php artisan cache:clear / optimize:clear will delete your browscap tables if you point the cache at storage/framework/cache. The re-download is tens of megabytes and can take a minute or more. Use a dedicated directory (storage/app/browscap) or a persistent Redis DB, and add it to your deploy's shared-path list.update() failure leaves you confidently misidentifying traffic forever.get_browser() is a different thing. PHP's native get_browser() requires browscap= in php.ini, is dramatically slower, and is unavailable on most managed hosts. Don't drift into it because the names look similar.isMobile() is a coarse UA heuristic — foldables, tablets, iPadOS Safari (which reports as macOS), and in-app webviews all break it. If you need real device signals, prefer Client Hints (Sec-CH-UA-*) with UA as fallback.CachedBrowscap that returns canned arrays. Otherwise CI flakes whenever browscap.org is slow.Verdict for a Laravel team: the bundle is dead weight — archived, Symfony 2-only, three stars, no dependents. The capability is still worth ~30 lines of provider/command glue around browscap/browscap-php if you genuinely need server-side UA classification. If you only need "is this a bot" or "is this mobile," ask whether crawler-detect, Client Hints, or plain responsive CSS gets you there with far less operational surface area first.
How can I help you explore Laravel packages today?