• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

mimmi20 / browser-detector / 18531733666

15 Oct 2025 02:08PM UTC coverage: 95.76% (-0.5%) from 96.217%
18531733666

push

github

web-flow
Merge pull request #1004 from mimmi20/updates

add new devices

193 of 233 new or added lines in 23 files covered. (82.83%)

3 existing lines in 1 file now uncovered.

7996 of 8350 relevant lines covered (95.76%)

13.39 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

98.94
/src/Detector.php
1
<?php
2

3
/**
4
 * This file is part of the browser-detector package.
5
 *
6
 * Copyright (c) 2012-2025, Thomas Mueller <mimmi20@live.de>
7
 *
8
 * For the full copyright and license information, please view the LICENSE
9
 * file that was distributed with this source code.
10
 */
11

12
declare(strict_types = 1);
13

14
namespace BrowserDetector;
15

16
use BrowserDetector\Cache\CacheInterface;
17
use BrowserDetector\Loader\Data\ClientData;
18
use BrowserDetector\Loader\Data\DeviceData;
19
use BrowserDetector\Loader\DeviceLoaderFactoryInterface;
20
use BrowserDetector\Parser\Header\Exception\VersionContainsDerivateException;
21
use BrowserDetector\Version\ForcedNullVersion;
22
use BrowserDetector\Version\NullVersion;
23
use BrowserDetector\Version\VersionInterface;
24
use Override;
25
use Psr\Http\Message\MessageInterface;
26
use Psr\Log\LoggerInterface;
27
use Psr\SimpleCache\InvalidArgumentException;
28
use UaDeviceType\Type;
29
use UaLoader\BrowserLoaderInterface;
30
use UaLoader\Data\ClientDataInterface;
31
use UaLoader\Data\DeviceDataInterface;
32
use UaLoader\EngineLoaderInterface;
33
use UaLoader\Exception\NotFoundException;
34
use UaLoader\PlatformLoaderInterface;
35
use UaRequest\GenericRequestInterface;
36
use UaRequest\Header\HeaderInterface;
37
use UaRequest\RequestBuilderInterface;
38
use UaResult\Bits\Bits;
39
use UaResult\Browser\Browser;
40
use UaResult\Company\Company;
41
use UaResult\Device\Architecture;
42
use UaResult\Device\Device;
43
use UaResult\Device\Display;
44
use UaResult\Engine\Engine;
45
use UaResult\Engine\EngineInterface;
46
use UaResult\Os\Os;
47
use UaResult\Os\OsInterface;
48
use UnexpectedValueException;
49

50
use function array_filter;
51
use function array_map;
52
use function assert;
53
use function explode;
54
use function is_array;
55
use function mb_strtolower;
56
use function reset;
57
use function sprintf;
58
use function str_contains;
59

60
final readonly class Detector implements DetectorInterface
61
{
62
    /**
63
     * sets the cache used to make the detection faster
64
     *
65
     * @throws void
66
     */
67
    public function __construct(
38✔
68
        /**
69
         * an logger instance
70
         */
71
        private LoggerInterface $logger,
72
        private CacheInterface $cache,
73
        private RequestBuilderInterface $requestBuilder,
74
        private DeviceLoaderFactoryInterface $deviceLoaderFactory,
75
        private PlatformLoaderInterface $platformLoader,
76
        private BrowserLoaderInterface $browserLoader,
77
        private EngineLoaderInterface $engineLoader,
78
    ) {
79
        // nothing to do
80
    }
38✔
81

82
    /**
83
     * Gets the information about the browser by User Agent
84
     *
85
     * @param array<non-empty-string, non-empty-string>|GenericRequestInterface|MessageInterface|string $headers
86
     *
87
     * @return array<mixed>
88
     *
89
     * @throws InvalidArgumentException
90
     * @throws UnexpectedValueException
91
     */
92
    #[Override]
38✔
93
    public function getBrowser(array | GenericRequestInterface | MessageInterface | string $headers): array
94
    {
95
        $request = $this->requestBuilder->buildRequest($headers);
38✔
96
        $cacheId = $request->getHash();
38✔
97

98
        if ($this->cache->hasItem($cacheId)) {
38✔
99
            $item = $this->cache->getItem($cacheId);
1✔
100
            assert(is_array($item));
1✔
101

102
            return $item;
1✔
103
        }
104

105
        $item = $this->parse($request);
37✔
106

107
        $this->cache->setItem($cacheId, $item);
37✔
108

109
        return $item;
37✔
110
    }
111

112
    /**
113
     * @return array{headers: array<non-empty-string, string>, device: array{architecture: string|null, deviceName: string|null, marketingName: string|null, manufacturer: string|null, brand: string|null, dualOrientation: bool|null, simCount: int|null, display: array{width: int|null, height: int|null, touch: bool|null, size: float|null}, type: string|null, ismobile: bool, istv: bool, bits: int|null}, os: array{name: string|null, marketingName: string|null, version: string|null, manufacturer: string|null}, client: array{name: string|null, version: string|null, manufacturer: string|null, type: string|null, isbot: bool}, engine: array{name: string|null, version: string|null, manufacturer: string|null}}
114
     *
115
     * @throws UnexpectedValueException
116
     */
117
    private function parse(GenericRequestInterface $request): array
37✔
118
    {
119
        $engineCodename  = null;
37✔
120
        $filteredHeaders = $request->getHeaders();
37✔
121

122
        /* detect device */
123
        $deviceIsMobile = $this->getDeviceIsMobile(filteredHeaders: $filteredHeaders);
37✔
124

125
        $deviceData = $this->getDeviceData(filteredHeaders: $filteredHeaders);
37✔
126

127
        $device              = $deviceData->getDevice();
37✔
128
        $deviceMarketingName = $device->getMarketingName();
37✔
129

130
        /* detect platform */
131
        $platform = $this->getPlatformData(
37✔
132
            filteredHeaders: $filteredHeaders,
37✔
133
            platformCodenameFromDevice: $deviceData->getOs(),
37✔
134
        );
37✔
135

136
        $platformName          = $platform->getName();
37✔
137
        $platformMarketingName = $platform->getMarketingName();
37✔
138

139
        if (mb_strtolower($platformName ?? '') === 'ios') {
37✔
140
            $engineCodename = 'webkit';
6✔
141

142
            try {
143
                $version    = $platform->getVersion();
6✔
144
                $iosVersion = $version->getVersion(VersionInterface::IGNORE_MINOR);
6✔
145

146
                if (
147
                    $deviceMarketingName !== null
6✔
148
                    && str_contains(mb_strtolower($deviceMarketingName), 'ipad')
6✔
149
                    && $iosVersion >= 13
6✔
150
                ) {
151
                    $platformName          = 'iPadOS';
2✔
152
                    $platformMarketingName = 'iPadOS';
6✔
153
                }
UNCOV
154
            } catch (UnexpectedValueException $e) {
×
UNCOV
155
                $this->logger->info($e);
×
156
            }
157
        }
158

159
        /* detect client */
160
        $clientData = $this->getClientData(filteredHeaders: $filteredHeaders);
37✔
161

162
        $client = $clientData->getClient();
37✔
163

164
        /* detect engine */
165
        $engine = $this->getEngineData(
37✔
166
            filteredHeaders: $filteredHeaders,
37✔
167
            engineCodename: $engineCodename,
37✔
168
            engineCodenameFromClient: $clientData->getEngine(),
37✔
169
        );
37✔
170

171
        $architecture = $this->getDeviceArchitecture($filteredHeaders);
37✔
172
        $deviceBits   = $this->getDeviceBitness($filteredHeaders);
37✔
173

174
        return [
37✔
175
            'headers' => array_map(
37✔
176
                callback: static fn (HeaderInterface $header): string => $header->getValue(),
37✔
177
                array: $request->getHeaders(),
37✔
178
            ),
37✔
179
            'device' => [
37✔
180
                'architecture' => $architecture === Architecture::unknown ? null : $architecture->value,
37✔
181
                'deviceName' => $device->getDeviceName(),
37✔
182
                'marketingName' => $device->getMarketingName(),
37✔
183
                'manufacturer' => $device->getManufacturer()->getType(),
37✔
184
                'brand' => $device->getBrand()->getType(),
37✔
185
                'dualOrientation' => $device->getDualOrientation(),
37✔
186
                'simCount' => $device->getSimCount(),
37✔
187
                'display' => $device->getDisplay()->toArray(),
37✔
188
                'type' => $device->getType()->getType(),
37✔
189
                'ismobile' => $deviceIsMobile ?? $device->getType()->isMobile(),
37✔
190
                'istv' => $device->getType()->isTv(),
37✔
191
                'bits' => $deviceBits === Bits::unknown ? null : $deviceBits->value,
37✔
192
            ],
37✔
193
            'os' => [
37✔
194
                'name' => $platformName,
37✔
195
                'marketingName' => $platformMarketingName,
37✔
196
                'version' => $platform->getVersion()->getVersion(),
37✔
197
                'manufacturer' => $platform->getManufacturer()->getType(),
37✔
198
                'bits' => $deviceBits === Bits::unknown ? null : $deviceBits->value,
37✔
199
            ],
37✔
200
            'client' => [
37✔
201
                'name' => $client->getName(),
37✔
202
                'modus' => null,
37✔
203
                'version' => $client->getVersion()->getVersion(),
37✔
204
                'manufacturer' => $client->getManufacturer()->getType(),
37✔
205
                'type' => $client->getType()->getType(),
37✔
206
                'isbot' => $client->getType()->isBot(),
37✔
207
                'bits' => null,
37✔
208
            ],
37✔
209
            'engine' => [
37✔
210
                'name' => $engine->getName(),
37✔
211
                'version' => $engine->getVersion()->getVersion(),
37✔
212
                'manufacturer' => $engine->getManufacturer()->getType(),
37✔
213
            ],
37✔
214
        ];
37✔
215
    }
216

217
    /**
218
     * @param array<non-empty-string, HeaderInterface> $filteredHeaders
219
     *
220
     * @throws void
221
     */
222
    private function getDeviceArchitecture(array $filteredHeaders): Architecture
37✔
223
    {
224
        $headersWithDeviceArchitecture = array_filter(
37✔
225
            $filteredHeaders,
37✔
226
            static fn (HeaderInterface $header): bool => $header->hasDeviceArchitecture(),
37✔
227
        );
37✔
228

229
        $deviceArchitectureHeader = reset($headersWithDeviceArchitecture);
37✔
230

231
        if ($deviceArchitectureHeader instanceof HeaderInterface) {
37✔
232
            return $deviceArchitectureHeader->getDeviceArchitecture();
1✔
233
        }
234

235
        return Architecture::unknown;
36✔
236
    }
237

238
    /**
239
     * @param array<non-empty-string, HeaderInterface> $filteredHeaders
240
     *
241
     * @throws void
242
     */
243
    private function getDeviceBitness(array $filteredHeaders): Bits
37✔
244
    {
245
        $headersWithDeviceBitness = array_filter(
37✔
246
            $filteredHeaders,
37✔
247
            static fn (HeaderInterface $header): bool => $header->hasDeviceBitness(),
37✔
248
        );
37✔
249

250
        $deviceBitnessHeader = reset($headersWithDeviceBitness);
37✔
251

252
        if ($deviceBitnessHeader instanceof HeaderInterface) {
37✔
253
            return $deviceBitnessHeader->getDeviceBitness();
1✔
254
        }
255

256
        return Bits::unknown;
36✔
257
    }
258

259
    /**
260
     * @param array<non-empty-string, HeaderInterface> $filteredHeaders
261
     *
262
     * @throws void
263
     */
264
    private function getDeviceIsMobile(array $filteredHeaders): bool | null
37✔
265
    {
266
        $headersWithDeviceMobile = array_filter(
37✔
267
            $filteredHeaders,
37✔
268
            static fn (HeaderInterface $header): bool => $header->hasDeviceIsMobile(),
37✔
269
        );
37✔
270

271
        $deviceMobileHeader = reset($headersWithDeviceMobile);
37✔
272

273
        if ($deviceMobileHeader instanceof HeaderInterface) {
37✔
274
            return $deviceMobileHeader->getDeviceIsMobile();
17✔
275
        }
276

277
        return null;
20✔
278
    }
279

280
    /**
281
     * @param array<non-empty-string, HeaderInterface> $filteredHeaders
282
     *
283
     * @throws void
284
     */
285
    private function getEngineVersion(array $filteredHeaders, string | null $engineCodename): VersionInterface
37✔
286
    {
287
        $headersWithEngineVersion = array_filter(
37✔
288
            $filteredHeaders,
37✔
289
            static fn (HeaderInterface $header): bool => $header->hasEngineVersion(),
37✔
290
        );
37✔
291

292
        $engineVersionHeader = reset($headersWithEngineVersion);
37✔
293

294
        if ($engineVersionHeader instanceof HeaderInterface) {
37✔
295
            return $engineVersionHeader->getEngineVersion($engineCodename);
21✔
296
        }
297

298
        return new NullVersion();
16✔
299
    }
300

301
    /**
302
     * detect the engine data
303
     *
304
     * @param array<non-empty-string, HeaderInterface> $filteredHeaders
305
     *
306
     * @throws void
307
     */
308
    private function getEngineData(
37✔
309
        array $filteredHeaders,
310
        string | null $engineCodename,
311
        string | null $engineCodenameFromClient,
312
    ): EngineInterface {
313
        $engineHeader  = null;
37✔
314
        $engineVersion = null;
37✔
315

316
        if ($engineCodename === null) {
37✔
317
            $headersWithEngineName = array_filter(
31✔
318
                $filteredHeaders,
31✔
319
                static fn (HeaderInterface $header): bool => $header->hasEngineCode(),
31✔
320
            );
31✔
321

322
            $engineHeader = reset($headersWithEngineName);
31✔
323

324
            if ($engineHeader instanceof HeaderInterface) {
31✔
325
                $engineCodename = $engineHeader->getEngineCode();
16✔
326
            }
327
        }
328

329
        if ($engineCodename === null) {
37✔
330
            $engineCodename = $engineCodenameFromClient;
15✔
331
        }
332

333
        $engineVersion = $this->getEngineVersion($filteredHeaders, $engineCodename);
37✔
334

335
        if ($engineCodename !== null) {
37✔
336
            try {
337
                $engine = $this->engineLoader->load(
22✔
338
                    key: $engineCodename,
22✔
339
                    useragent: $engineHeader instanceof HeaderInterface ? $engineHeader->getValue() : '',
22✔
340
                );
22✔
341

342
                if ($engineVersion->getVersion() !== null) {
21✔
343
                    return $engine->withVersion($engineVersion);
14✔
344
                }
345

346
                return $engine;
7✔
347
            } catch (UnexpectedValueException $e) {
1✔
348
                $this->logger->info($e);
1✔
349
            }
350
        }
351

352
        return new Engine(
16✔
353
            name: null,
16✔
354
            manufacturer: new Company(type: 'unknown', name: null, brandname: null),
16✔
355
            version: $engineVersion,
16✔
356
        );
16✔
357
    }
358

359
    /**
360
     * @param array<non-empty-string, HeaderInterface> $filteredHeaders
361
     *
362
     * @throws void
363
     */
364
    private function getClientVersion(array $filteredHeaders, string | null $clientCodename): VersionInterface
37✔
365
    {
366
        $headersWithClientVersion = array_filter(
37✔
367
            $filteredHeaders,
37✔
368
            static fn (HeaderInterface $header): bool => $header->hasClientVersion(),
37✔
369
        );
37✔
370

371
        $clientVersionHeader = reset($headersWithClientVersion);
37✔
372

373
        if ($clientVersionHeader instanceof HeaderInterface) {
37✔
374
            return $clientVersionHeader->getClientVersion($clientCodename);
18✔
375
        }
376

377
        return new NullVersion();
19✔
378
    }
379

380
    /**
381
     * detect the client data
382
     *
383
     * @param array<non-empty-string, HeaderInterface> $filteredHeaders
384
     *
385
     * @throws void
386
     */
387
    private function getClientData(array $filteredHeaders): ClientDataInterface
37✔
388
    {
389
        $clientCodename = null;
37✔
390
        $clientVersion  = new NullVersion();
37✔
391

392
        $headersWithClientCode = array_filter(
37✔
393
            $filteredHeaders,
37✔
394
            static fn (HeaderInterface $header): bool => $header->hasClientCode(),
37✔
395
        );
37✔
396

397
        $clientHeader = reset($headersWithClientCode);
37✔
398

399
        if ($clientHeader instanceof HeaderInterface) {
37✔
400
            $clientCodename = $clientHeader->getClientCode();
19✔
401
        }
402

403
        $clientVersion = $this->getClientVersion($filteredHeaders, $clientCodename);
37✔
404

405
        if ($clientCodename !== null) {
37✔
406
            assert($clientHeader instanceof HeaderInterface);
18✔
407

408
            try {
409
                $clientData = $this->browserLoader->load(
18✔
410
                    key: $clientCodename,
18✔
411
                    useragent: $clientHeader->getValue(),
18✔
412
                );
18✔
413

414
                if ($clientVersion->getVersion() !== null) {
17✔
415
                    $client = $clientData->getClient();
16✔
416

417
                    return new ClientData(
16✔
418
                        client: $client->withVersion($clientVersion),
16✔
419
                        engine: $clientData->getEngine(),
16✔
420
                    );
16✔
421
                }
422

423
                return $clientData;
1✔
424
            } catch (UnexpectedValueException $e) {
1✔
425
                $this->logger->info($e);
1✔
426
            }
427
        }
428

429
        return new ClientData(
20✔
430
            client: new Browser(
20✔
431
                name: null,
20✔
432
                manufacturer: new Company(type: 'unknown', name: null, brandname: null),
20✔
433
                version: $clientVersion,
20✔
434
                type: \UaBrowserType\Type::Unknown,
20✔
435
                bits: Bits::unknown,
20✔
436
                modus: null,
20✔
437
            ),
20✔
438
            engine: null,
20✔
439
        );
20✔
440
    }
441

442
    /**
443
     * @param array<non-empty-string, HeaderInterface> $filteredHeaders
444
     *
445
     * @throws VersionContainsDerivateException
446
     */
447
    private function getPlatformVersion(array $filteredHeaders, string | null $platformCodename): VersionInterface
37✔
448
    {
449
        $headersWithPlatformVersion = array_filter(
37✔
450
            $filteredHeaders,
37✔
451
            static fn (HeaderInterface $header): bool => $header->hasPlatformVersion(),
37✔
452
        );
37✔
453

454
        $platformHeaderVersion = reset($headersWithPlatformVersion);
37✔
455

456
        if ($platformHeaderVersion instanceof HeaderInterface) {
37✔
457
            return $platformHeaderVersion->getPlatformVersion($platformCodename);
26✔
458
        }
459

460
        return new NullVersion();
11✔
461
    }
462

463
    /**
464
     * detect the platform data
465
     *
466
     * @param array<non-empty-string, HeaderInterface> $filteredHeaders
467
     *
468
     * @throws void
469
     */
470
    private function getPlatformData(array $filteredHeaders, string | null $platformCodenameFromDevice): OsInterface
37✔
471
    {
472
        $platformCodename = null;
37✔
473
        $platformVersion  = null;
37✔
474

475
        $headersWithPlatformCode = array_filter(
37✔
476
            $filteredHeaders,
37✔
477
            static fn (HeaderInterface $header): bool => $header->hasPlatformCode(),
37✔
478
        );
37✔
479

480
        $platformHeader = reset($headersWithPlatformCode);
37✔
481

482
        if ($platformHeader instanceof HeaderInterface) {
37✔
483
            $platformCodename = $platformHeader->getPlatformCode();
26✔
484
        }
485

486
        if ($platformCodename === null) {
37✔
487
            $platformCodename = $platformCodenameFromDevice;
17✔
488
        }
489

490
        try {
491
            $platformVersion = $this->getPlatformVersion($filteredHeaders, $platformCodename);
37✔
492
        } catch (VersionContainsDerivateException $e) {
1✔
493
            $platformVersion = null;
1✔
494
            $derivate        = $e->getDerivate();
1✔
495

496
            if ($platformHeader instanceof HeaderInterface && $derivate !== '') {
1✔
497
                $derivateCodename = $platformHeader->getPlatformCode($derivate);
1✔
498

499
                if ($derivateCodename !== null) {
1✔
500
                    $platformCodename = $derivateCodename;
1✔
501
                }
502
            }
503
        }
504

505
        // var_dump(4, $platformVersion);
506

507
        if ($platformCodename !== null) {
37✔
508
            try {
509
                $platform = $this->platformLoader->load(
27✔
510
                    key: $platformCodename,
27✔
511
                    useragent: $platformHeader instanceof HeaderInterface ? $platformHeader->getValue() : '',
27✔
512
                );
27✔
513

514
                if (
515
                    $platformVersion instanceof VersionInterface
26✔
516
                    && ($platformVersion instanceof ForcedNullVersion || $platformVersion->getVersion() !== null)
26✔
517
                ) {
518
                    return $platform->withVersion($platformVersion);
24✔
519
                }
520

521
                return $platform;
2✔
522
            } catch (UnexpectedValueException $e) {
1✔
523
                $this->logger->info($e);
1✔
524
            }
525
        }
526

527
        if (!$platformVersion instanceof VersionInterface) {
11✔
UNCOV
528
            $platformVersion = null;
×
529
        }
530

531
        return new Os(
11✔
532
            name: null,
11✔
533
            marketingName: null,
11✔
534
            manufacturer: new Company(type: 'unknown', name: null, brandname: null),
11✔
535
            version: $platformVersion ?? new NullVersion(),
11✔
536
            bits: Bits::unknown,
11✔
537
        );
11✔
538
    }
539

540
    /**
541
     * detect the device data
542
     *
543
     * @param array<non-empty-string, HeaderInterface> $filteredHeaders
544
     *
545
     * @throws void
546
     */
547
    private function getDeviceData(array $filteredHeaders): DeviceDataInterface
37✔
548
    {
549
        $headersWithDeviceCode = array_filter(
37✔
550
            $filteredHeaders,
37✔
551
            static fn (HeaderInterface $header): bool => $header->hasDeviceCode(),
37✔
552
        );
37✔
553

554
        $deviceHeader   = reset($headersWithDeviceCode);
37✔
555
        $deviceCodename = null;
37✔
556

557
        if ($deviceHeader instanceof HeaderInterface) {
37✔
558
            $deviceCodename = $deviceHeader->getDeviceCode();
28✔
559
        }
560

561
        if ($deviceCodename !== null) {
37✔
562
            [$company, $key] = explode('=', $deviceCodename, 2);
28✔
563

564
            try {
565
                $deviceLoader = ($this->deviceLoaderFactory)($company);
28✔
566

567
                return $deviceLoader->load($key);
28✔
568
            } catch (NotFoundException $e) {
1✔
569
                $this->logger->info(
1✔
570
                    new UnexpectedValueException(
1✔
571
                        sprintf('Device "%s" of Manufacturer "%s" was not found', $key, $company),
1✔
572
                        0,
1✔
573
                        $e,
1✔
574
                    ),
1✔
575
                );
1✔
576
            }
577
        }
578

579
        return new DeviceData(
10✔
580
            device: new Device(
10✔
581
                architecture: Architecture::unknown,
10✔
582
                deviceName: null,
10✔
583
                marketingName: null,
10✔
584
                manufacturer: new Company(type: 'unknown', name: null, brandname: null),
10✔
585
                brand: new Company(type: 'unknown', name: null, brandname: null),
10✔
586
                type: Type::Unknown,
10✔
587
                display: new Display(width: null, height: null, touch: null, size: null),
10✔
588
                dualOrientation: null,
10✔
589
                simCount: null,
10✔
590
                bits: Bits::unknown,
10✔
591
            ),
10✔
592
            os: null,
10✔
593
        );
10✔
594
    }
595
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc