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.
Architecture fit. This is a leaf-node utility library: under 200 lines, three regular expressions, no framework coupling, no I/O, no state, no external services. It has no architectural opinions, so it cannot conflict with a Laravel app's layering, but it also brings no architecture of its own. The correct mental model is "a pure function we vendor," not "a subsystem we integrate." It slots in wherever a User-Agent header string needs to become (platform, browser, browserVersion) — most commonly request middleware, auth/login audit logging, analytics enrichment, bot/crawler gating, and support tooling.
Integration feasibility. Very high. Composer install, requires php >=5.4.0 and ext-ctype, MIT license, no transitive dependency chain to audit. Three consumption surfaces exist and all work in Laravel unchanged: the deprecated global parse_user_agent(), the namespaced donatj\UserAgent\parse_user_agent(), and the UserAgentParser object wrapper. Because there is no service provider, config file, migration, or Facade, there is effectively nothing to "install" beyond the autoload entry. The realistic work is 50–150 lines of our own adapter code and tests, plus deciding where in the request lifecycle to call it.
Technical risk. Risk is concentrated in fidelity and lifecycle, not in stability:
| Risk | Severity | Mitigation |
|---|---|---|
| Regex-based detection drifts as UAs change (new Chromium variants, AI crawlers, in-app webviews) | Medium | Pin a version, track the "unknown/Other" rate as a metric, plan a refresh cadence |
| Deliberate non-goals: no OS version, Brave indistinguishable from Chrome, iPadOS 13+ identical to macOS | Medium–High if used for analytics or fraud | Do not promise OS version or device model to stakeholders; use client-side signals where it matters |
| UA strings are client-supplied and trivially spoofed | High for security use cases | Treat output as telemetry/hint only, never as an authorization or bot-blocking control |
| Maintainer concentration — effectively single-maintainer project | Medium | MIT + tiny surface area means a fork is cheap; keep our adapter thin so swapping is a one-file change |
| "Dependents: 0" in the supplied metadata | Low (metadata artifact) | Verify Packagist download/dependent counts before citing adoption internally |
| Reported release date of 2026-06-17 vs. an older v1.9.0 changelog | Low | Confirm the actual latest tag and changelog during PoC; do not assume feature parity |
The library's own README is unusually candid about limits, which is a positive signal for a TPM — the vendor has pre-negotiated scope with us. The counter-risk is that these limits get lost by the time a requirement ("show me iOS versions in the admin console") reaches a sprint.
Key questions.
Stack fit. Laravel 8–11 on PHP 8.x is comfortably supported; the library's floor (PHP 5.4) is irrelevant to us but means no modern-syntax incompatibility. Note the historical ext-json requirement added in v1.6.0 and the PHP 8.1 deprecation fix landed in v1.7.0 — anything below 1.7.0 should be excluded on modern PHP. No Laravel-specific glue exists, so we own the boundary.
Recommended boundary shape:
App\Support\UserAgent\AgentResolver, with one method taking an optional UA string and returning a readonly DTO (platform, browser, version, isKnown). The DTO is the contract; the library is the implementation detail.UserAgentParser with bind()/singleton() so tests can swap a fake resolver without touching regexes.$request->userAgent() at the call site, not inside the resolver — keeps the adapter pure and testable.Agent facade if multiple teams consume it, but resist adding one until a second consumer exists.fallback_on_unknown behaviour and a cache TTL if we memoize per-request.Migration path. Most Laravel shops arrive here from one of three places: raw get_browser() (browscap — slow, ini-dependent), jenssegers/agent (which wraps Mobile Detect and carries a much larger surface), or matomo/device-detector (heavier, signature-database-driven, far richer output). Migration is additive rather than surgical:
donatj/phpuseragentparser, once over the incumbent — and run them in parallel behind a feature flag, logging both outputs.If the incumbent is matomo/device-detector and requirements genuinely need device type or bot classification, the honest conclusion of the parallel run may be "do not migrate." Budget for that outcome.
Compatibility. Two compatibility surfaces matter: (a) our PHP/Laravel matrix — fine; (b) downstream consumers of the parsed fields. The 1.x line is explicitly backward compatible with 0.x, and the only breaking change in the project's history was a directory reshuffle in v0.13.0 affecting non-Composer users — not applicable to us. The deprecated global function still works, but new code should use the namespaced function or the OO wrapper so a future 2.x removal is a no-op for us.
Sequencing. Roughly one engineering week end-to-end for a single-squad adoption:
Maintenance. The maintenance burden is near-zero for the dependency and non-trivial-but-bounded for the adapter. Our real recurring cost is the UA corpus review — the library ships updated patterns when someone notices a new browser or crawler, so our job is to (a) stay roughly current on version pins and (b) watch the unknown rate. Expect this to be a few hours per quarter, not a standing allocation. The dependency itself is MIT, dependency-free, and small enough to fork into app/Support in an afternoon if upstream stalls, which materially lowers the abandonment risk that the single-maintainer profile would otherwise imply.
Support. Support surface is thin: the library has no configuration, no runtime dependencies, and no failure mode that requires a restart. The support burden lands on whoever owns the DTO semantics — specifically, on explaining to product stakeholders, repeatedly, why "OS version" cannot be provided. Document the non-goals (no OS version, no Brave, no iPadOS-after-13, no device model) in the same place as the API contract, not buried in a vendor README, or the same conversation recurs each quarter. Escalation path is upstream GitHub issues; given a single maintainer, do not assume SLA-bound responsiveness for non-security issues.
Scaling. Negligible. Three regexes against a short string is microseconds; there is no I/O, no network, no memory pressure, no connection pool to exhaust. Parsing per-request is affordable even at high throughput, though if it lands in a hot path (e.g. every API request in middleware) memoize per-request on the DTO rather than adding a distributed cache — the string is already in memory and the parse is cheaper than a cache round-trip. Do not build a Redis layer for this; that would invert the cost/benefit. Horizontal scaling is completely unaffected.
Failure modes. The dangerous failures are silent, not loud:
isKnown flag exists to force a decision at the call site.User-Agent is common from health checks and some SDKs; the resolver must not throw.donatj\UserAgent\parse_user_agent() directly across the codebase, the swap cost we deliberately engineered away returns. Enforce the boundary with a lint/review rule.Ramp-up. Essentially zero for any PHP developer — the README's usage block is complete in a dozen lines, and 100% unit test coverage upstream plus our own fixture corpus means contributors can change behaviour with confidence. Realistic ramp-up for a mid-level engineer: under an hour to first productive use, half a day to own the adapter and its tests. The non-obvious ramp-up is conceptual — teaching the team what UA parsing fundamentally cannot deliver, so that scope pressure surfaces at design time rather than as a mid-sprint surprise. That education, not the code, is the TPM's actual deliverable here.
How can I help you explore Laravel packages today?