pataar/browser-detect
Detect browsers, operating systems, devices, and bots from the user agent in your Laravel application. pataar/browser-detect provides a simple API to identify visitors and tailor responses, logging, or feature checks based on their platform.
pataar/browser-detect is a thin Laravel wrapper around matomo/device-detector. You get a Browser facade that parses the current request's User-Agent and answers questions about it.
composer require pataar/browser-detect
Laravel package discovery registers Pataar\BrowserDetect\BrowserDetectServiceProvider and the Browser facade alias automatically. Publish the config so you can tune caching:
php artisan vendor:publish --tag=config
# or: --provider="Pataar\BrowserDetect\BrowserDetectServiceProvider"
First use case — a diagnostic route to see what the detector reports for a real request:
use Browser;
Route::get('/whoami', fn () => [
'bot' => Browser::isBot(),
'mobile' => Browser::isMobile(),
'tablet' => Browser::isTablet(),
'desktop' => Browser::isDesktop(),
'browser' => Browser::browserName(), // "Chrome"
'version' => Browser::browserVersion(), // "126.0.6478.127"
'platform' => Browser::platformName(), // "iOS"
'device' => Browser::deviceType(), // "smartphone"
'family' => Browser::deviceFamily(), // "iPhone"
]);
Where to look first:
config/browser-detect.php — the only knob you'll touch (cache settings).Pataar\BrowserDetect\Agent — the object behind the facade; every Browser::x() call is forwarded to it. Read this class before assuming a method exists.Matomo\DeviceDetector\DeviceDetector upstream docs — the wrapper only exposes a subset; anything missing can be reached via Browser::getDeviceDetector()-style access or by parsing manually.Upgrading to 5.4.0: non-breaking. composer update pataar/browser-detect and, if you had published the config, diff it against the vendor copy — 5.4.0 introduces a new device-detector cache option that won't appear in your existing config file. Then php artisan config:clear (or config:cache again if you cache config in production).
Presentation-only branching (the common case). Use it for "show the app-store banner on mobile" rather than for behaviour that matters:
Route::get('/', function () {
return view(Browser::isMobile() ? 'home.mobile' : 'home.desktop');
});
Middleware for device-aware redirects. Keep it early in the stack so downstream code doesn't re-parse:
class RedirectMobileUsers
{
public function handle(Request $request, Closure $next)
{
if (! Browser::isBot() && Browser::isMobile() && ! $request->is('m/*')) {
return redirect('/m' . $request->getPathInfo());
}
return $next($request);
}
}
Blade helpers. Wrap the facade once so views stay readable and you have a single place to add caching or test doubles:
@if (Browser::isMobile())
<a href="{{ $iosUrl }}">Get the app</a>
@endif
Parsing a UA that isn't the current request. Browser::parse($ua) returns a fresh Agent for an arbitrary string — use this in imports, webhook handlers, and queue jobs:
$agent = Browser::parse($log->user_agent);
if ($agent->isBot()) {
return;
}
Testing. Don't stub the facade for simple cases — set the header and let the real detector run. It's deterministic:
$this->withHeader('User-Agent', 'Mozilla/5.0 (iPhone; CPU iPhone OS 17_5 like Mac OS X) ...')
->get('/')
->assertSee('Get the app');
For unit tests where you only care about the branch, Browser::shouldReceive('isMobile')->andReturn(true) works, but prefer Browser::parse() with a fixture UA so you also exercise your real-world UA strings.
Caching (the 5.4.0 headline). Device detection is regex-heavy — every request without a cache re-runs dozens of patterns. There are two layers worth understanding:
cache.lifetime style setting) memoises a parsed result per UA.Turn both on for any endpoint that sees real traffic:
// config/browser-detect.php — check the published file for exact keys after 5.4.0
'cache' => [
'lifetime' => 3600, // seconds; 0 disables the package-level cache
// new in 5.4.0: detector-internal cache wiring
// (store / prefix / ttl — names depend on the release, verify in the config)
],
Point the cache at Redis or another shared store if you run multiple app servers; a per-node array store gives you almost nothing.
isBot() first. Every other predicate is meaningless for crawlers, and isDesktop()/isMobile() will happily return a value for Googlebot.User-Agent is attacker-controlled. Never use this package for authorisation, rate limiting, or "mobile users get a cheaper plan" logic. It's for rendering decisions only.Browser::isMobile() describes whatever the worker's environment reports — nothing useful. Always use Browser::parse($storedUserAgent) there.Browser::setUserAgent() anywhere, reset it per request, or just prefer Browser::parse() which returns a fresh instance and can't bleed between requests.config:cache hides new keys. After upgrading to 5.4.0, a config-cached deployment will silently ignore the new cache option until you re-run php artisan config:cache. Verify with php artisan tinker → config('browser-detect').0 means "off", not "forever". It's a common misread when copying configs between projects.matomo/device-detector (or the wrapper) and the cache still holds results parsed by the old regex database, you can serve stale/incorrect device data for the cache TTL. Flush the tags/prefix you configured for this package on deploy.isBot() covers the known-bot list; custom scrapers, headless Chrome with a spoofed UA, and new AI crawlers routinely slip through. Log the UAs you see in production and revisit periodically.matomo/device-detector, which is why pataar/browser-detect releases follow upstream. Keep it on a scheduled update rather than pinning for a year.Agent class doesn't expose, drop to the underlying DeviceDetector instance rather than patching the package — your fork will be the thing you forget to update.$request->userAgent() — proxies and CDNs occasionally rewrite or strip the header, and you'll be debugging the detector for something that never arrived.How can I help you explore Laravel packages today?