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

Phpuseragentparser Laravel Package

donatj/phpuseragentparser

A tiny, fast PHP user-agent parser that detects all modern browsers with just three regular expressions and under 200 lines of code. Installable via Composer, fully unit tested, and easy to use.

View on GitHub
Deep Wiki
Context7

Getting Started

Install

composer require donatj/phpuseragentparser

The package requires ext-ctype (and, per the composer manifest, ext-json). Both ship with a default PHP/Laravel install, but if you run in a stripped-down Docker image, confirm them in composer check-platform-reqs.

Where to look first

  1. The README "Usage" block — two APIs, pick one and stick to it:
    • Procedural: donatj\UserAgent\parse_user_agent() → array keyed by PLATFORM, BROWSER, BROWSER_VERSION constants.
    • Object-oriented: new UserAgentParser() then $parser->parse() (or $parser()) → object with platform(), browser(), browserVersion().
  2. donatj\UserAgent\Platforms and donatj\UserAgent\Browsers — these constant classes are your vocabulary for comparisons. Read them before writing any string literals like 'Chrome'.
  3. Skip the os_version_detection branch. It exists, it's not merged, and the README explicitly discourages relying on UA for OS versions.

First use case — "what is this visitor on?"

The fastest win in a Laravel app is a one-off inspection. In php artisan tinker:

$parser = new \donatj\UserAgent\UserAgentParser();
$ua = $parser->parse('Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X) ...');

$ua->platform();       // "iPhone"
$ua->browser();        // "Safari"
$ua->browserVersion(); // "17.0"

From there, the natural first production step is a tiny middleware or a Request macro that exposes the parsed result to controllers and Blade, plus a database column (platform, browser, browser_version) on whatever model records visits, orders, or logins. That single column set unlocks device-mix reporting, support triage, and "why is this customer seeing the broken layout" debugging — which is 80% of what most teams actually need from a UA parser.

Implementation Patterns

Bind the parser once, resolve everywhere

// AppServiceProvider::register()
$this->app->scoped(\donatj\UserAgent\UserAgentParser::class, fn () => new \donatj\UserAgent\UserAgentParser());

Use scoped, not singleton — see the Octane note in Gotchas. The parser is stateless, but scoped keeps the semantics honest and lets you swap a fake in tests.

A thin application-level wrapper

Don't let donatj\... namespaces leak into 40 files. Wrap it in something that speaks your domain:

namespace App\Support;

use donatj\UserAgent\{UserAgentParser, Browsers, Platforms};

class UserAgent
{
    public function __construct(private UserAgentParser $parser) {}

    public function parse(?string $ua): array
    {
        $parsed = $this->parser->parse($ua ?? '');

        return [
            'platform' => $parsed->platform() ?: 'unknown',
            'browser'  => $parsed->browser() ?: 'unknown',
            'version'  => $parsed->browserVersion() ?: '',
        ];
    }
}

Normalising empty/null to 'unknown' at the boundary means your analytics queries never have to deal with NULL vs '' vs false.

Request macro for ergonomic access

// AppServiceProvider::boot()
Request::macro('userAgentInfo', function () {
    return app(\App\Support\UserAgent::class)->parse($this->userAgent());
});

Then $request->userAgentInfo()['browser'] anywhere — controllers, form requests, middleware. Beware: recomputes each call. If you call it multiple times per request, memoize inside the wrapper or stash it as a request attribute in middleware.

Middleware that attaches, rather than computes

public function handle(Request $request, Closure $next)
{
    $request->attributes->set('ua', app(\App\Support\UserAgent::class)->parse($request->userAgent()));
    return $next($request);
}

Attaching to attributes (not the session) keeps it per-request and avoids serialising it into session storage. Register it on the web group only, or better, only on routes that need it — a global middleware on a high-traffic API is wasted work.

Blade convenience

@php($ua = request()->userAgentInfo())
<html data-browser="{{ $ua['browser'] }}" data-platform="{{ $ua['platform'] }}">

Emitting it as a data- attribute lets front-end JS and your CSS target it ([data-platform="iPhone"]), which is far cheaper than shipping a JS UA library.

Bot / AI-crawler gating

This is the package's most underrated use in 2025-era Laravel apps. The Browsers constant list now includes GPTBOT, CHATGPT_USER, OAI_SEARCHBOT, GOOGLEBOT, GOOGLEBOT_IMAGE, GOOGLEBOT_VIDEO, BINGBOT, APPLEBOT, SLACKBOT, TWITTERBOT, TELEGRAMBOT, FACEBOOKEXTERNALHIT, MASTODON, plus CURL and WGET.

const AI_CRAWLERS = [
    Browsers::GPTBOT, Browsers::CHATGPT_USER, Browsers::OAI_SEARCHBOT,
];

if (in_array($ua['browser'], AI_CRAWLERS, true)) {
    // serve a different robots policy, skip analytics, or block
}

Because Browsers::GOOGLEBOT and Browsers::HEADLESSCHROME are distinct from Chrome, you can separate "real user" analytics from "crawler" analytics without writing a single regex.

Feature-flag / legacy-browser prompting

Store minimum supported versions in config, compare against version_compare():

// config/browsers.php
'minimum' => ['Chrome' => '110', 'Firefox' => '110', 'Safari' => '16', 'Edge' => '110'],
$min = config("browsers.minimum.{$ua['browser']}");
$outdated = $min && version_compare($ua['version'], $min, '<');

This drives an "upgrade your browser" banner or a degraded-but-functional layout. Keep the config file, not hardcoded checks scattered in controllers.

Device class for layout and analytics

Derive a coarse bucket (mobile / tablet / desktop) from the platform constants — IPHONE, IPOD, ANDROID, BLACKBERRY, KINDLE_FIRE → mobile; IPAD, KINDLE, PLAYBOOK → tablet; else desktop. Store the bucket alongside the raw platform so reporting queries stay simple and stable even if you upgrade the package and detection shifts.

Persist snapshots, not computed values

When recording platform/browser/version on an Order or a LoginEvent, write the raw strings, not a derived boolean like is_mobile. Booleans calcify; strings can be re-derived with new logic later.

Testing

$this->withHeaders(['User-Agent' => 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) ... Chrome/120.0.0.0 Safari/537.36'])
     ->get('/')
     ->assertSee('Chrome');

Laravel's withHeaders is the cleanest hook. In unit tests, feed the parser a literal UA string and assert against Browsers::CHROME / Platforms::WINDOWS constants rather than raw strings — it makes the test self-documenting and catches constant renames on upgrade.

Gotchas and Tips

Never use this for security decisions. UA strings are trivially spoofed. Bot detection via UA is a heuristic for analytics and UX, not an access control boundary. Pair with rate limiting, signed tokens, or a real bot-management layer if you're defending anything.

parse() with no argument reads $_SERVER['HTTP_USER_AGENT']. In a queued job, an artisan command, a scheduled task, or a webhook handler without the header, that's absent — you'll silently get empty/null values. Always pass the UA explicitly: $parser->parse($request->userAgent() ?? ''). Bake this into your wrapper so nobody can call the ambient version by accident.

Nulls and empty strings are normal. A missing UA, a health-check probe, or an exotic client yields empty platform/browser. Coalesce to 'unknown' at exactly one place in your codebase and never downstream.

Brave is Chrome. By design — Brave sends an identical UA. Any "Brave users" metric you build will read as zero. Do not attempt a UA-based workaround.

iPadOS 13+ is macOS. Apple hardened this in 14; the strings are identical. Tablet-vs-desktop detection for iPads is impossible from UA alone — you need a client-side hint or the Sec-CH-UA client hints (not handled by this package). Set expectations with product accordingly.

No OS versions, deliberately. "Windows 10" vs "Windows 11" is not available and won't be. If someone asks for OS version reporting, point them at client-side JS or accept the limitation. Adding your own Windows version regex reintroduces exactly the maintenance burden the author rejected.

Use the namespaced function or, better, the object API. The global parse_user_agent() is deprecated as of 1.x. It still works (no BC break), but new code should use \donatj\UserAgent\parse_user_agent() or UserAgentParser. Don't mix both styles in one codebase.

Case sensitivity bites. Edge detection was made case-sensitive in v1.6.1 specifically to fix false positives. If you're comparing $ua['browser'] against your own hardcoded strings, use the constants — Browsers::EDGE — and ===, never strtolower() comparisons against literals you typed.

singleton + Octane/RoadRunner is a trap. If you resolve the parser (or your wrapper holding parsed state) as a singleton and cache the parsed result on the instance, that value can leak across requests in a long-lived worker. Use scoped, or cache per-request only inside the request lifecycle.

Don't cache parsed UA results across deployments. If you memoise browser in Redis keyed by UA string, remember that a package upgrade can change the output for the same input (new browser support, fixed detection order). Version your cache key with the package version, or just re-parse — it's three regexes and genuinely fast.

HeadlessChrome vs Chrome is a gift. Browsers::HEADLESSCHROME is separately detectable. In analytics, counting HeadlessChrome as a real user inflates engagement metrics and burns your conversion funnel. Filter it in your reporting layer.

Truncate UA before storing. Real-world UA strings are usually <250 chars but can be arbitrarily long and are attacker-controlled. Column size VARCHAR(512) and truncate on write — otherwise you'll get the occasional MySQL strict-mode exception or a bloated logs table.

Version strings can be three digits. There are tests for three-digit majors; don't assume x.y.z when parsing browserVersion() yourself. Use version_compare(), and always guard against '' (which version_compare handles, but your explode('.') might not).

Extension point: wrap, don't fork. The package is ~200 lines and intentionally minimal. If you need something it doesn't do (client hints, custom in-app browser detection, a corporate-client UA), add a first-class check in your wrapper that runs before or after the package and returns a normalised array. Forking means you own the regexes forever; the wrapper means you can drop it the day upstream catches up.

Debugging tip: log the raw UA string (truncated) whenever your parsed output looks surprising. The parser is deterministic and correct for its input — nine times out of ten the "bug" is that a proxy, CDN, or test client sent a UA you didn't expect, not that detection failed.

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.
socialdept/atp-orm
cboxdk/laravel-assistant-client
socialdept/atp-resolver
socialdept/tenant-domains
tautvydasr/blade-class-names
socialdept/color-scales
maherelgamil/controller
coleus/widgets
coleus/table
coleus/users
coleus/music
coleus/health
coleus/calendar
coleus/notes
coleus/support
coleus/settings
coleus/apps
seampact/laravel
emmyconceptng/laravel-ng-postcode
jjsquady/round-robin