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

Device Detector Laravel Package

piwik/device-detector

Device Detector is a universal library that parses User Agents and Browser Client Hints to detect devices, browsers, operating systems, brands, and models. It identifies desktop, tablet, mobile, TV, car, and console devices plus clients like feed readers and media players.

View on GitHub
Deep Wiki
Context7

Getting Started

Install the package — there is no Laravel service provider, it is a plain framework-agnostic PHP library:

composer require matomo/device-detector

A minimal, working call inside a controller or middleware:

use DeviceDetector\ClientHints;
use DeviceDetector\DeviceDetector;

$dd = new DeviceDetector(
    $request->userAgent() ?? '',
    ClientHints::factory($request->server->all()) // optional but recommended
);
$dd->parse();

if ($dd->isBot()) {
    $bot = $dd->getBot();            // ['name' => 'Googlebot', 'category' => 'Search bot', ...]
} else {
    $dd->getClient();                // ['type' => 'browser', 'name' => 'Chrome', 'version' => '126.0', 'engine' => 'Blink']
    $dd->getOs();                    // ['name' => 'Android', 'version' => '14', 'platform' => '']
    $dd->getDeviceName();            // int constant, e.g. 1 (smartphone)
    $dd->getBrandName();             // 'Samsung'
    $dd->getModel();                 // 'Galaxy S24'
}

Where to look first:

  1. The DeviceDetector\DeviceDetector class itself — the getters and the is*() helpers (isSmartphone(), isTablet(), isTV(), isBrowser(), isMobileApp(), …) are the whole public surface.
  2. DeviceDetector\ClientHints — how client hints are read from server vars.
  3. regexes/*.yml in the vendor directory — everything is regex-driven YAML; this is also what gets cached.
  4. DeviceDetector\Parser\ — you can use individual parsers (Bot, OperatingSystem, Client\Browser, Device\AbstractDeviceParser) standalone when you only need one answer.

The highest-value first use case in a Laravel app is a bot filter: parse the UA once early (middleware or a form request), drop/flag known crawlers, and skip the expensive parts of your stack (analytics writes, search indexing, throttling counters). The second is persisting device_type / os / client alongside logins and page views so you can segment later without re-parsing history.

Implementation Patterns

Bind a factory, never the detector itself. DeviceDetector holds per-user-agent state; putting it in the container as a singleton will leak the first request's result into every later request (especially under Octane). Bind a small factory that returns a freshly configured instance:

// app/Support/DeviceDetection/DeviceDetectorFactory.php
final class DeviceDetectorFactory
{
    public function __construct(private CacheInterface $cache) {}

    public function make(string $userAgent, array $server): DeviceDetector
    {
        $dd = new DeviceDetector($userAgent, ClientHints::factory($server));
        $dd->setCache($this->cache);
        $dd->setYamlParser(new \DeviceDetector\Yaml\Symfony()); // if symfony/yaml is installed
        return $dd;
    }
}

Register it in a service provider and set the global version-truncation policy there once:

public function register(): void
{
    AbstractDeviceParser::setVersionTruncation(AbstractDeviceParser::VERSION_TRUNCATION_NONE);

    $this->app->singleton(DeviceDetectorFactory::class, fn () => new DeviceDetectorFactory(
        new LaravelStoreCache('redis') // see Gotchas for why not always LaravelCache
    ));

    // scoped: one DeviceContext per request/queue job, reset automatically under Octane
    $this->app->scoped(DeviceContext::class, fn ($app) => new DeviceContext(
        $app->make(DeviceDetectorFactory::class),
        $app['request']
    ));
}

Wrap parsing in a lazy, request-scoped context object. This keeps parsing out of requests that never need it (health checks, asset routes, webhooks):

final class DeviceContext
{
    private ?DeviceDetector $dd = null;

    public function __construct(private DeviceDetectorFactory $factory, private Request $request) {}

    public function detector(): DeviceDetector
    {
        return $this->dd ??= tap(
            $this->factory->make($this->request->userAgent() ?? '', $this->request->server->all()),
            fn (DeviceDetector $d) => $d->parse()
        );
    }

    public function isBot(): bool { return $this->detector()->isBot(); }
    public function type(): string { return AbstractDeviceParser::getDeviceName($this->detector()->getDeviceName()); }
}

Cache the parsed result per user agent, not just the regexes. The built-in cache layer mainly accelerates loading the YAML regex sets; if you are on the hot path, memoize your normalized array by UA + hints fingerprint:

$fingerprint = md5($ua.'|'.($server['HTTP_SEC_CH_UA'] ?? '').'|'.($server['HTTP_SEC_CH_UA_PLATFORM'] ?? ''));
$result = Cache::store('redis')->remember("dd:v6:$fingerprint", now()->addDays(30), function () use ($ua, $server) {
    $dd = $this->factory->make($ua, $server);
    $dd->parse();
    return [ /* normalized scalars only — do not cache the object */ ];
});

Send the Accept-CH header or client hints will simply never arrive. A global middleware is the right place:

$response->headers->set('Accept-CH', implode(', ', [
    'Sec-CH-UA', 'Sec-CH-UA-Mobile', 'Sec-CH-UA-Platform', 'Sec-CH-UA-Platform-Version',
    'Sec-CH-UA-Model', 'Sec-CH-UA-Full-Version-List', 'Sec-CH-UA-Arch',
]));
$response->headers->set('Vary', 'Sec-CH-UA, Sec-CH-UA-Mobile, Sec-CH-UA-Platform', false);

Add Critical-CH with the same list if you need platform version on the very first request. Client hints are only sent on secure origins, so this is a no-op locally over HTTP.

Persist a denormalized snapshot next to the raw UA string. Store device_type, device_brand, device_model, os_name, os_version, client_name, client_version, is_bot, bot_category as plain indexed columns. Normalize the integer constant to a string once:

AbstractDeviceParser::getDeviceName($dd->getDeviceName()); // 1 => 'smartphone'
OperatingSystem::getOsFamily($dd->getOs('name'));          // 'Android' => 'Android'
Browser::getBrowserFamily($dd->getClient('name'));         // 'Chrome Mobile' => 'Chrome'

The family helpers are what you actually want for reporting — otherwise your "Chrome" segment fragments into dozens of near-duplicate client names.

Use the standalone BotParser for cheap gating. If all you need is "is this a crawler?", skip the full detector (it loads far fewer regex files):

$bot = new \DeviceDetector\Parser\Bot();
$bot->setUserAgent($request->userAgent() ?? '');
$bot->discardDetails();               // returns bool instead of an array; faster
if ($bot->parse()) { abort(403); }

Typical homes for that: public form submissions, registration, outbound-link redirects, sitemap/robots endpoints, and analytics ingestion.

Queue pattern: capture the UA string and the handful of Sec-CH-* server values at request time, push a job, and parse inside the worker. Never re-read $_SERVER in a queued job, and never parse at query time in a report — store the snapshot.

Testing: freeze user agents as data providers and assert on the normalized array, not on the raw detector. Remember $_SERVER is empty in tests, so construct ClientHints from an explicit array:

$dd = new DeviceDetector($ua, ClientHints::factory(['HTTP_SEC_CH_UA_MOBILE' => '?1']));

Gotchas and Tips

  • parse() must be called explicitly. Don't rely on getters triggering detection; call parse() once, immediately after construction.
  • Version truncation is global static state. The default returns X.Y only. Call AbstractDeviceParser::setVersionTruncation(...) before any parsing (a service provider register() is fine), and be aware it persists across tests in the same process.
  • discardBotInformation() and skipBotDetection() change semantics. The first makes getBot() return true instead of an array; the second makes crawlers look like ordinary devices. Only one of those is what you want — pick deliberately.
  • Bot UAs masquerade as browsers. isBot() must be checked before getClient(); a UA containing "Chrome" can still be a crawler. Never infer bot-ness from a missing/odd client name.
  • Missing or empty UA. Pass '', not null, and treat getClient() === null / getOs() === null as "unknown" rather than an error. Client hints can still identify the platform on modern Chrome even with a stripped UA.
  • Client hints require server cooperation. Without Accept-CH (and HTTPS), ClientHints::factory() yields an empty object and Android models / Windows 11 / macOS versions degrade. Also note ClientHints::factory() expects a $_SERVER-shaped array — $request->server->all() works, $request->headers->all() does not.
  • DeviceDetector\Cache\LaravelCache uses your default cache store. If the default is file, you get lock contention and slow reads. Either point the default store at Redis/APCu or implement the small DeviceDetector\Cache\CacheInterface against a named store (Cache::store('redis')) — check the installed interface's exact method signatures before you write it.
  • Flush the detector cache on package upgrades. Regex sets change frequently (new devices ship weekly); stale cached regex data means stale detection. Include the package major version in your own result-cache keys (dd:v6:...) and clear the detector's cache namespace on deploy.
  • Don't cache the DeviceDetector instance or reuse it across requests. It is stateful. Under Octane, scoped bindings are correct; singletons are a silent data-corruption bug.
  • "Class Spyc not found". The default YAML parser is Spyc, which is not always pulled in. Add mustangostang/spyc, or — cleaner in a Laravel app — composer require symfony/yaml and call $dd->setYamlParser(new \DeviceDetector\Yaml\Symfony()).
  • Parsing is regex-heavy (~0.5–2 ms warm). Fine for logins and page-view ingestion; not fine to run several times per request or inside a loop over rows. Parse once, store scalars.
  • getBrandName() / getModel() are frequently empty. Model detection is best-effort and heavily dependent on client hints (Sec-CH-UA-Model). Never build required logic on model being present; always have an "Unknown" bucket.
  • Don't use detection for authorization or security decisions. UA strings are trivially spoofed. Use it for presentation (mobile layout, feature degradation), analytics segmentation, and soft bot throttling — paired with behavioural signals.
  • Pin and update the package. Detection quality is a function of regex freshness; a version from six months ago will misclassify current devices. A monthly composer update matomo/device-detector (or Dependabot PR) is the single biggest quality lever.
  • License: LGPL-3.0. Using it as an unmodified Composer dependency from a closed-source Laravel app is fine. If you patch the library itself and distribute the result, those changes must be shared — keep customisations in your own wrapper classes instead.
  • Debugging tip: to see why something classified oddly, dump the whole picture at once — $dd->getBot(), getClient(), getOs(), getDeviceName(), getBrandName(), getModel() — plus the raw UA and hints, and keep that dump in your log context when you record a misdetection, so you can file it upstream with a reproducible UA.
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.
microsoft/fluentui-system-icons
mmccook/php-json-canonicalization-scheme
middlewares/firewall
qubiqx/montapacking
nyoncode/laravel-package-console
pnscripts/filament-broadquill-lite
pnscripts/laravel-sentfold-lite
vleap/mx-sdk-laravel-internal
twidpay/utils
htetmyathlaing/scout-elasticsearch-driver
artisan-toolbox/front-awesome
ls/client-assistant
users-au/laravel-client
cgv/localized-resource
noith/settings
kda/laravel-package
kda/filament-dynamic-navigation
bypersoft/signbox
pairus/product-data
fayne/laravel-filament-cms-core