Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Browser Detector Laravel Package

sinergi/browser-detector

Lightweight PHP library for detecting browser, operating system, device, and language from user agent strings. Uses targeted regex rules instead of a bloated UA database, with simple detectors and optional Laravel service provider and facade integration.

View on GitHub
Deep Wiki
Context7

Getting Started

Install it as a plain Composer dependency — there is no first-party Laravel service provider to publish, so wiring is up to you:

composer require sinergi/browser-detector

Pin ^6.1.4 (or run composer update sinergi/browser-detector on an existing install): that release is the one that teaches the parser about Chromium Edge. On 6.1.3 and earlier an Edge UA can be misclassified, so any stored browser name/version from those versions is not directly comparable with what you'll get now.

Everything you need lives in one namespace with six small classes. Read these first, in this order:

Class What it answers
Sinergi\BrowserDetector\Browser name, version, isChrome(), isFirefox(), isSafari(), isIE(), isOpera(), isRobot(); Edge surfaces as getName() === Browser::EDGE
Sinergi\BrowserDetector\Os isWindows(), isMacintosh(), isLinux(), isAndroid(), isIos()
Sinergi\BrowserDetector\Device isMobile(), isTablet(), plus getName()
Sinergi\BrowserDetector\Language two-letter language from the request
Sinergi\BrowserDetector\AcceptLanguage parsed Accept-Language header, ordered by q-value
Sinergi\BrowserDetector\UserAgent the raw UA string wrapper

Every detector accepts an optional user-agent — either a raw string or a UserAgent instance — and falls back to $_SERVER['HTTP_USER_AGENT'] when you pass nothing. Always pass the UA explicitly in Laravel; don't rely on the superglobal.

The first use case worth building is a tiny request-scoped service so views, middleware, and logs all agree:

// app/Providers/DetectionServiceProvider.php
public function register(): void
{
    $this->app->scoped(Browser::class, fn () => new Browser(request()->userAgent()));
    $this->app->scoped(Os::class,      fn () => new Os(request()->userAgent()));
    $this->app->scoped(Device::class,  fn () => new Device(request()->userAgent()));
}
$browser = app(Browser::class);
$browser->getName();     // Sinergi\BrowserDetector\Browser::CHROME — or ::EDGE on Chromium Edge
$browser->getVersion();  // "120.0.6099.109"
$browser->isChrome();    // true

Before you build anything on top of it, run one real Edge UA through it (Edg/ on desktop/Android, EdgiOS/ on iOS) and confirm what getName() returns in your installed version — that single check tells you whether the 6.1.4 fix is live.

Implementation Patterns

Middleware that tags the request once. Parse the UA a single time per request and stash a plain array or DTO on the request; every downstream consumer reads that instead of re-instantiating detectors.

class DetectClient
{
    public function handle(Request $request, Closure $next): Response
    {
        $ua = (string) $request->userAgent();
        $userAgent = new UserAgent($ua);

        $browser = new Browser($userAgent);
        $os      = new Os($userAgent);
        $device  = new Device($userAgent);

        $request->attributes->set('client', [
            'browser'         => $browser->getName(),
            'browser_version' => $browser->getVersion(),
            'os'              => $os->getName(),
            'os_version'      => $os->getVersion(),
            'mobile'          => $device->isMobile(),
            'tablet'          => $device->isTablet(),
        ]);

        return $next($request);
    }
}

Progressive-enhancement Blade helpers. Wrap the booleans in directives so templates stay readable:

Blade::if('browser', fn (string $name) => app(Browser::class)->getName() === $name);
Blade::if('mobile', fn () => app(Device::class)->isMobile());
@browser(\Sinergi\BrowserDetector\Browser::SAFARI)
    <p>Add to Home Screen for the best experience.</p>
@endbrowser

Any directive that branches on a specific browser is a place the Edge change can bite you: a Chromium Edge user is no longer "Chrome" for @browser(...) purposes, so an @browser(Browser::CHROME) block silently stops rendering for them after the upgrade. Prefer capability/intent names (@modernBrowser, @supportsInstallPrompt) over brand names in templates.

Gate an unsupported-browser page. Compare with version_compare() and guard against UNKNOWN first — never assume getVersion() is numeric:

$browser = app(Browser::class);

if ($browser->isIE()
    && $browser->getName() !== Browser::UNKNOWN
    && version_compare((string) $browser->getVersion(), '11', '<')) {
    abort(426, 'Please upgrade your browser.');
}

Locale negotiation with AcceptLanguage. Feed it the header explicitly and walk the ordered list against your supported locales, falling back to config('app.fallback_locale'):

$header = $request->header('Accept-Language', '');
$preferred = (new AcceptLanguage($header))->getLanguage(); // ordered by q-value

$locale = collect($preferred)
    ->map(fn ($l) => substr($l, 0, 2))
    ->first(fn ($l) => in_array($l, config('app.supported_locales'), true))
    ?? config('app.fallback_locale');

Analytics and audit logging. Persist browser, browser_version, os, and mobile alongside sessions or login events — but truncate the version to the major number. Storing full versions produces thousands of distinct values and destroys the usefulness of any group-by. Note that a Chromium Edge UA reports the Edge version token (e.g. 120.0.2210.91), not the underlying Chromium build, so Edge and Chrome major numbers drift apart even on the same engine.

Deterministic tests. Constructors take strings, so UA parsing is trivially unit-testable. Use data providers of real UA strings and assert names/versions; never mutate $_SERVER globally in a test suite. Add explicit cases for the shapes you care about most — Windows Edge Chromium, macOS Edge Chromium, Android Edge, iOS Edge — and assert both the name and that isChrome() does not come back true. Those four asserts are your regression net for the 6.1.4 behaviour and for any future UA change.

Gotchas and Tips

  • Maintenance is minimal, not dead. 6.1.4 is the first release since September 2021, and it is a single community PR adding Chromium Edge support — read that as "stable, occasionally patched", not "actively developed". Pin the version (^6.1.4), treat every result as best-effort, and reach for matomo/device-detector or whichbrowser/php if accuracy matters more than the small footprint.
  • Edge now has its own identity. After upgrading, Chromium Edge resolves to Browser::EDGE instead of falling in with Chrome or coming back UNKNOWN. Anything you keyed off isChrome() — feature flags, CSS classes, bug workarounds, analytics funnels — needs to be re-checked against Edge traffic, and historical reports will show a step change on the upgrade date.
  • Three different Edge UA tokens. Desktop/Android Chromium Edge sends Edg/, iOS Edge sends EdgiOS/, and legacy EdgeHTML sends Edge/. Verify each shape you actually receive rather than assuming one fix covers all three — iOS in particular is a common UNKNOWN.
  • No User-Agent Client Hints support. Chrome's UA reduction and Sec-CH-UA headers are invisible to this library. Even with Edge fixed, this remains the main long-term failure mode: the classic UA is being deliberately degraded.
  • iPadOS 13+ lies. The default "Request Desktop Website" UA is macOS Safari, so an iPad is reported as Os::OSX / desktop Safari. If tablet-vs-desktop matters, supplement with a feature/JS hint or accept the noise.
  • iOS browsers carry non-standard tokens (CriOS, FxiOS, EdgiOS). Verify isChrome()/isFirefox() against a real iOS UA before you build feature gates on them — the underlying engine is always WebKit.
  • Empty UA means UNKNOWN. Queue workers, console commands, health checks, and many tests have no HTTP_USER_AGENT; the fallback to $_SERVER may also emit an undefined-index notice. Always pass the string.
  • Laravel Octane: singleton() bindings capturing request()->userAgent() will leak the first request's UA into every subsequent request. Use scoped() (as above) or resolve manually inside the middleware.
  • UA strings are attacker-controlled. Use detection for rendering, localization defaults, and analytics — never for authorization or security decisions.
  • Compare versions defensively. getName() can be UNKNOWN; versions can come back empty or with odd suffixes. Always run version_compare() on a sanitized string, not on raw output.
  • Don't hand-roll the is* checks. A Chrome UA contains Safari; an Edge UA contains Chrome. The library already orders its internal matching correctly — reimplementing that logic in your own regex is exactly where the next Edge-style regression comes from.
  • Instantiate once. Parsing involves iterating a pattern list; calling new Browser() inside a Blade loop is wasteful. The scoped bindings above solve this, and the result can also be cached on the request object.
  • Language vs AcceptLanguage: Language gives you the single top preference, AcceptLanguage gives the ordered list. Use the latter for negotiation, keeping an explicit user-chosen locale in session as the winner over any header-derived guess.
  • Robot detection lives on Browser (isRobot()) and is regex-based — useful for suppressing analytics pings, not for blocking traffic.
Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
elsayed85/laravel-api-exceptions
ouhssini/laravel-demo-mode
daworks/ncloud-outbound-mailer-driver
gp247/cms
ignibyte/aic-gates
nexa-merchant/blog
novasa/laravel-language-center
opencage/geocode
adolfoholzer/parameters
livenetworks/ln-starter
shitoudev/permissionmanager
johannesclimacus/laravel-tui
hocvt/seotools
vitalybaev/sypexgeo
amphibee/themosis-ignition
gito-me/fast-excel
frankyso/laravel-ipaymu
geolize-studio/laravel-chile
boomerangui/chat
cgv/localized-resource