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

mimmi20 / browser-detector / 21661000504

04 Feb 2026 06:22AM UTC coverage: 95.964% (+0.006%) from 95.958%
21661000504

Pull #1033

github

mimmi20
add new clients, add new devices
Pull Request #1033: add new client

82 of 82 new or added lines in 9 files covered. (100.0%)

12 existing lines in 1 file now uncovered.

12317 of 12835 relevant lines covered (95.96%)

61.43 hits per line

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

94.0
/src/Collection/Headers.php
1
<?php
2

3
/**
4
 * This file is part of the browser-detector package.
5
 *
6
 * Copyright (c) 2012-2026, 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\Collection;
15

16
use BrowserDetector\Loader\Data\ClientData;
17
use BrowserDetector\Loader\Data\DeviceData;
18
use BrowserDetector\Loader\DeviceLoaderFactoryInterface;
19
use BrowserDetector\Parser\Header\Exception\VersionContainsDerivateException;
20
use BrowserDetector\Version\Exception\NotNumericException;
21
use BrowserDetector\Version\FireOs;
22
use BrowserDetector\Version\ForcedNullVersion;
23
use BrowserDetector\Version\LineageOs;
24
use BrowserDetector\Version\NullVersion;
25
use BrowserDetector\Version\VersionBuilder;
26
use BrowserDetector\Version\VersionInterface;
27
use Psr\Log\LoggerInterface;
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 UaResult\Bits\Bits;
38
use UaResult\Browser\Browser;
39
use UaResult\Company\Company;
40
use UaResult\Device\Architecture;
41
use UaResult\Device\Device;
42
use UaResult\Device\Display;
43
use UaResult\Device\FormFactor;
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_first;
52
use function array_key_exists;
53
use function array_last;
54
use function array_map;
55
use function assert;
56
use function explode;
57
use function in_array;
58
use function is_string;
59
use function sprintf;
60

61
final readonly class Headers
62
{
63
    /** @var array<non-empty-string, HeaderInterface> */
64
    private array $headers;
65

66
    /** @throws void */
67
    public function __construct(
131✔
68
        GenericRequestInterface $request,
69
        private LoggerInterface $logger,
70
        private DeviceLoaderFactoryInterface $deviceLoaderFactory,
71
        private PlatformLoaderInterface $platformLoader,
72
        private BrowserLoaderInterface $browserLoader,
73
        private EngineLoaderInterface $engineLoader,
74
    ) {
75
        $this->headers = $request->getHeaders();
131✔
76
    }
77

78
    /** @throws void */
79
    public function getDeviceArchitecture(): Architecture
131✔
80
    {
81
        $headersWithDeviceArchitecture = array_filter(
131✔
82
            $this->headers,
131✔
83
            static fn (HeaderInterface $header): bool => $header->hasDeviceArchitecture(),
131✔
84
        );
131✔
85

86
        $deviceArchitectureHeader = array_first($headersWithDeviceArchitecture);
131✔
87

88
        if ($deviceArchitectureHeader instanceof HeaderInterface) {
131✔
89
            return $deviceArchitectureHeader->getDeviceArchitecture();
6✔
90
        }
91

92
        return Architecture::unknown;
125✔
93
    }
94

95
    /** @throws void */
96
    public function getDeviceFormFactor(): FormFactor
131✔
97
    {
98
        $headersWithDeviceFormFactors = array_filter(
131✔
99
            $this->headers,
131✔
100
            static fn (HeaderInterface $header): bool => $header->hasDeviceFormFactor(),
131✔
101
        );
131✔
102

103
        $deviceFormFactorsHeader = array_first($headersWithDeviceFormFactors);
131✔
104

105
        if ($deviceFormFactorsHeader instanceof HeaderInterface) {
131✔
106
            return array_last(
15✔
107
                $deviceFormFactorsHeader->getDeviceFormFactor(),
15✔
108
            ) ?? FormFactor::unknown;
15✔
109
        }
110

111
        return FormFactor::unknown;
116✔
112
    }
113

114
    /** @throws void */
115
    public function getDeviceBitness(): Bits
131✔
116
    {
117
        $headersWithDeviceBitness = array_filter(
131✔
118
            $this->headers,
131✔
119
            static fn (HeaderInterface $header): bool => $header->hasDeviceBitness(),
131✔
120
        );
131✔
121

122
        $deviceBitnessHeader = array_first($headersWithDeviceBitness);
131✔
123

124
        if ($deviceBitnessHeader instanceof HeaderInterface) {
131✔
125
            return $deviceBitnessHeader->getDeviceBitness();
6✔
126
        }
127

128
        return Bits::unknown;
125✔
129
    }
130

131
    /** @throws void */
132
    public function getDeviceIsWow64(): bool | null
131✔
133
    {
134
        $headersWithDeviceIsWow64 = array_filter(
131✔
135
            $this->headers,
131✔
136
            static fn (HeaderInterface $header): bool => $header->hasDeviceIsWow64(),
131✔
137
        );
131✔
138

139
        $deviceIsWow64Header = array_first($headersWithDeviceIsWow64);
131✔
140

141
        if ($deviceIsWow64Header instanceof HeaderInterface) {
131✔
142
            return $deviceIsWow64Header->getDeviceIsWow64();
21✔
143
        }
144

145
        return null;
110✔
146
    }
147

148
    /** @throws void */
149
    public function getDeviceIsMobile(): bool | null
122✔
150
    {
151
        $headersWithDeviceMobile = array_filter(
122✔
152
            $this->headers,
122✔
153
            static fn (HeaderInterface $header): bool => $header->hasDeviceIsMobile(),
122✔
154
        );
122✔
155

156
        $deviceMobileHeader = array_first($headersWithDeviceMobile);
122✔
157

158
        if ($deviceMobileHeader instanceof HeaderInterface) {
122✔
159
            return $deviceMobileHeader->getDeviceIsMobile();
67✔
160
        }
161

162
        return null;
55✔
163
    }
164

165
    /**
166
     * detect the engine data
167
     *
168
     * @throws void
169
     */
170
    public function getEngineData(
131✔
171
        \UaData\EngineInterface $engine,
172
        string | null $engineCodenameFromClient,
173
    ): EngineInterface {
174
        $engineHeader = null;
131✔
175

176
        if ($engine === \BrowserDetector\Data\Engine::unknown) {
131✔
177
            $headersWithEngineName = array_filter(
122✔
178
                $this->headers,
122✔
179
                static fn (HeaderInterface $header): bool => $header->hasEngineCode(),
122✔
180
            );
122✔
181

182
            $engineHeader = array_first($headersWithEngineName);
122✔
183

184
            if ($engineHeader instanceof HeaderInterface) {
122✔
185
                try {
186
                    $engine = $engineHeader->getEngineCode();
121✔
187
                } catch (\UaRequest\Exception\NotFoundException) {
×
188
                    // do nothing
189
                }
190
            }
191
        }
192

193
        if ($engine === \BrowserDetector\Data\Engine::unknown) {
131✔
194
            try {
195
                $engine = \BrowserDetector\Data\Engine::fromName((string) $engineCodenameFromClient);
16✔
196
            } catch (UnexpectedValueException) {
×
197
                // do nothing
198
            }
199
        }
200

201
        $engineVersion = $this->getEngineVersion($engine);
131✔
202

203
        if ($engine !== \BrowserDetector\Data\Engine::unknown) {
131✔
204
            try {
205
                $engineFrom = $this->engineLoader->loadFromEngine(
120✔
206
                    engine: $engine,
120✔
207
                    useragent: $engineHeader instanceof HeaderInterface ? $engineHeader->getValue() : '',
120✔
208
                );
120✔
209

210
                if ($engineVersion->getVersion() !== null) {
120✔
211
                    return $engineFrom->withVersion($engineVersion);
110✔
212
                }
213

214
                return $engineFrom;
10✔
215
            } catch (UnexpectedValueException $e) {
×
216
                $this->logger->info($e);
×
217
            }
218
        }
219

220
        return new Engine(
11✔
221
            name: null,
11✔
222
            manufacturer: new Company(type: 'unknown', name: null, brandname: null),
11✔
223
            version: $engineVersion,
11✔
224
        );
11✔
225
    }
226

227
    /**
228
     * detect the client data
229
     *
230
     * @throws void
231
     *
232
     * @phpcs:disable SlevomatCodingStandard.Functions.FunctionLength.FunctionLength
233
     */
234
    public function getClientData(): ClientDataInterface
131✔
235
    {
236
        $headersWithClientCode = array_filter(
131✔
237
            $this->headers,
131✔
238
            static fn (HeaderInterface $header): bool => $header->hasClientCode(),
131✔
239
        );
131✔
240

241
        $clientCodes = array_map(
131✔
242
            static fn (HeaderInterface $clientHeader): string | null => $clientHeader->getClientCode(),
131✔
243
            $headersWithClientCode,
131✔
244
        );
131✔
245

246
        $firstClientCodename = array_first($clientCodes);
131✔
247
        $clientCodename      = $firstClientCodename;
131✔
248
        $chromeClientVersion = null;
131✔
249

250
        if (is_string($firstClientCodename)) {
131✔
251
            switch ($firstClientCodename) {
252
                case 'android webview':
130✔
253
                    $lastClientCodename = array_last($clientCodes);
3✔
254
                    $clientHeader       = array_first($headersWithClientCode);
3✔
255

256
                    if (
257
                        is_string($lastClientCodename)
3✔
258
                        && $lastClientCodename !== $firstClientCodename
3✔
259
                    ) {
260
                        $clientCodename = $lastClientCodename;
3✔
261
                        $clientHeader   = array_last($headersWithClientCode);
3✔
262
                    }
263

264
                    break;
3✔
265
                case 'chromium':
127✔
266
                    $lastClientCodename = array_last($clientCodes);
22✔
267
                    $clientHeader       = array_first($headersWithClientCode);
22✔
268

269
                    if (is_string($lastClientCodename)) {
22✔
270
                        switch ($lastClientCodename) {
271
                            case 'opera':
22✔
272
                            case 'silk':
21✔
273
                            case 'adblock browser':
19✔
274
                            case 'google-nest-hub':
18✔
275
                            case 'chrome for ios':
18✔
276
                            case 'ecosia':
17✔
277
                            case 'firefox':
16✔
278
                            case 'headline bot':
15✔
279
                            case 'hubspot crawler':
14✔
280
                            case 'headless-chrome':
13✔
281
                            case 'samsungbrowser':
12✔
282
                            case 'edge':
11✔
283
                            case 'google-search':
10✔
284
                            case 'iron':
9✔
285
                            case 'aol desktop':
8✔
286
                            case 'qtwebengine':
7✔
287
                                $clientCodename = $lastClientCodename;
16✔
288
                                $clientHeader   = array_last($headersWithClientCode);
16✔
289

290
                                break;
16✔
291
                            case 'chrome':
6✔
292
                                $headersWithPlatformCode = array_filter(
6✔
293
                                    $this->headers,
6✔
294
                                    static fn (HeaderInterface $header): bool => $header->hasPlatformCode(),
6✔
295
                                );
6✔
296

297
                                try {
298
                                    $platformCodes = array_map(
6✔
299
                                        static fn (HeaderInterface $platformHeader): \UaData\OsInterface => $platformHeader->getPlatformCode(),
6✔
300
                                        $headersWithPlatformCode,
6✔
301
                                    );
6✔
302
                                } catch (\UaRequest\Exception\NotFoundException) {
×
303
                                    $platformCodes = [\BrowserDetector\Data\Os::unknown];
×
304
                                }
305

306
                                if (($platformCodes['sec-ch-ua-platform'] ?? null) === \BrowserDetector\Data\Os::linux) {
6✔
307
                                    $clientCodename = $lastClientCodename;
1✔
308
                                    $clientHeader   = array_last($headersWithClientCode);
1✔
309

310
                                    $clientVersions      = $this->getClientVersions($clientCodename);
1✔
311
                                    $chromeClientVersion = array_last($clientVersions);
1✔
312
                                }
313

314
                                break;
6✔
315
                            default:
316
                                // do nothing
317
                        }
318
                    }
319

320
                    break;
22✔
321
                case 'chrome':
105✔
322
                    $lastClientCodename = array_last($clientCodes);
26✔
323
                    $clientHeader       = array_first($headersWithClientCode);
26✔
324

325
                    if (is_string($lastClientCodename)) {
26✔
326
                        switch ($lastClientCodename) {
327
                            case 'opera':
26✔
328
                            case 'silk':
26✔
329
                            case 'adblock browser':
26✔
330
                            case 'google-nest-hub':
26✔
331
                            case 'chrome for ios':
25✔
332
                            case 'ecosia':
25✔
333
                            case 'duck-assist-bot':
25✔
334
                            case 'sogou web spider':
24✔
335
                            case 'googlebot':
23✔
336
                            case 'google-search':
22✔
337
                            case 'webpagetest':
21✔
338
                            case 'facebook lite':
20✔
339
                            case 'lighthouse':
19✔
340
                            case 'pageburst':
18✔
341
                                $clientCodename = $lastClientCodename;
9✔
342
                                $clientHeader   = array_last($headersWithClientCode);
9✔
343

344
                                break;
9✔
345
                            default:
346
                                // do nothing
347
                        }
348
                    }
349

350
                    break;
26✔
351
                case 'headless-chrome':
79✔
352
                    $lastClientCodename = array_last($clientCodes);
9✔
353
                    $clientHeader       = array_first($headersWithClientCode);
9✔
354

355
                    if (is_string($lastClientCodename)) {
9✔
356
                        switch ($lastClientCodename) {
357
                            case 'amazon bot':
9✔
358
                            case 'facebookexternalhit':
8✔
359
                            case 'headline bot':
7✔
360
                            case 'hanalei-bot':
6✔
361
                            case 'statistik-hessen':
5✔
362
                            case 'claudebot':
4✔
363
                                $clientCodename = $lastClientCodename;
6✔
364
                                $clientHeader   = array_last($headersWithClientCode);
6✔
365

366
                                break;
6✔
367
                            default:
368
                                // do nothing
369
                        }
370
                    }
371

372
                    break;
9✔
373
                case 'brave':
70✔
374
                    $lastClientCodename = array_last($clientCodes);
1✔
375
                    $clientHeader       = array_first($headersWithClientCode);
1✔
376

377
                    if (is_string($lastClientCodename)) {
1✔
378
                        switch ($lastClientCodename) {
379
                            case 'pageburst':
1✔
380
                                $clientCodename = $lastClientCodename;
1✔
381
                                $clientHeader   = array_last($headersWithClientCode);
1✔
382

383
                                break;
1✔
384
                            default:
385
                                // do nothing
386
                        }
387
                    }
388

389
                    break;
1✔
390
                case 'huawei-browser':
69✔
391
                    $lastClientCodename = array_last($clientCodes);
4✔
392
                    $clientHeader       = array_first($headersWithClientCode);
4✔
393

394
                    if (is_string($lastClientCodename)) {
4✔
395
                        switch ($lastClientCodename) {
396
                            case 'huawei-mobile-services':
4✔
397
                                $clientCodename = $lastClientCodename;
2✔
398
                                $clientHeader   = array_last($headersWithClientCode);
2✔
399

400
                                break;
2✔
401
                            default:
402
                                // do nothing
403
                        }
404
                    }
405

406
                    break;
4✔
407
                case 'keplr-app':
65✔
408
                case 'lookr-app':
64✔
409
                case 'kimi-app':
63✔
410
                case 'opera mini':
62✔
411
                case 'baidu box app lite':
61✔
412
                case 'baidu box app':
60✔
413
                case 'nytimes-crossword':
58✔
414
                case 'visha':
57✔
415
                case 'search-craft':
56✔
416
                case 'duckduck app':
55✔
417
                    $clientHeader = array_last($headersWithClientCode);
12✔
418

419
                    break;
12✔
420
                default:
421
                    $clientHeader = array_first($headersWithClientCode);
53✔
422

423
                    break;
53✔
424
            }
425

426
            assert($clientHeader instanceof HeaderInterface);
130✔
427

428
            $clientVersions = $this->getClientVersions($clientCodename);
130✔
429
            $clientVersion  = match ($clientCodename) {
130✔
430
                'aloha-browser', 'opera touch', 'adblock browser', 'opera mini', 'baidu box app lite', 'opera', 'silk', 'mint browser', 'instagram app', 'bingsearch', 'stargon-browser', 'yahoo! japan', 'hi-search', 'pi browser', 'soul-browser', 'kik', 'oupeng browser', 'snapchat app', 'reddit-app', 'nytimes-crossword', 'smart-life', 'firefox', 'duck-assist-bot', 'sogou web spider', 'headline bot', 'amazon bot', 'hubspot crawler', 'facebookexternalhit', 'opera mobile', 'miui browser', 'stoutner-privacy-browser', 'dogtorance-app', 'line', 'msn-app', 'pageburst', 'googlebot', 'google-search', 'webpagetest', 'hanalei-bot', 'facebook lite', 'lighthouse', 'samsungbrowser', 'statistik-hessen', 'iron', 'facebook app', 'huawei-browser', 'aol desktop', 'huawei-mobile-services', 'claudebot', 'opera gx', 'qtwebengine' => $clientVersions['user-agent']
61✔
431
                    ?? array_last($clientVersions),
61✔
432
                'duckduck app', 'ucbrowser', 'edge', 'headless-chrome' => $clientVersions['sec-ch-ua-full-version-list']
13✔
433
                    ?? $clientVersions['sec-ch-ua']
13✔
434
                    ?? $clientVersions['sec-ch-ua-full-version']
13✔
435
                    ?? array_last($clientVersions),
13✔
436
                'ecosia' => $clientVersions['sec-ch-ua-full-version']
1✔
437
                    ?? array_last($clientVersions),
1✔
438
                'vivaldi' => $clientVersions['sec-ch-ua']
1✔
439
                    ?? array_last($clientVersions),
1✔
440
                'chrome' => $chromeClientVersion
18✔
441
                    ?? $clientVersions['sec-ch-ua-full-version-list']
18✔
442
                    ?? $clientVersions['sec-ch-ua']
18✔
443
                    ?? $clientVersions['sec-ch-ua-full-version']
18✔
444
                    ?? array_last($clientVersions),
18✔
445
                default => array_first($clientVersions),
36✔
446
            };
130✔
447

448
            try {
449
                $clientData = $this->browserLoader->load(
130✔
450
                    key: (string) $clientCodename,
130✔
451
                    useragent: $clientHeader->getValue(),
130✔
452
                );
130✔
453

454
                if ($clientVersion?->getVersion() !== null) {
130✔
455
                    $client = $clientData->getClient();
110✔
456

457
                    return new ClientData(
110✔
458
                        client: $client->withVersion($clientVersion),
110✔
459
                        engine: $clientData->getEngine(),
110✔
460
                    );
110✔
461
                }
462

463
                return $clientData;
20✔
UNCOV
464
            } catch (UnexpectedValueException $e) {
×
465
                $this->logger->info($e);
×
466
            }
467
        }
468

469
        return new ClientData(
1✔
470
            client: new Browser(
1✔
471
                name: null,
1✔
472
                manufacturer: new Company(type: 'unknown', name: null, brandname: null),
1✔
473
                version: new NullVersion(),
1✔
474
                type: \UaBrowserType\Type::Unknown,
1✔
475
                bits: Bits::unknown,
1✔
476
            ),
1✔
477
            engine: null,
1✔
478
        );
1✔
479
    }
480

481
    /**
482
     * detect the platform data
483
     *
484
     * @throws void
485
     */
486
    public function getPlatformData(string | null $platformCodenameFromDevice): OsInterface
131✔
487
    {
488
        $headersWithPlatformCode = array_filter(
131✔
489
            $this->headers,
131✔
490
            static fn (HeaderInterface $header): bool => $header->hasPlatformCode(),
131✔
491
        );
131✔
492

493
        try {
494
            $platformCodes = array_map(
131✔
495
                static fn (HeaderInterface $platformHeader): \UaData\OsInterface => $platformHeader->getPlatformCode(),
131✔
496
                $headersWithPlatformCode,
131✔
497
            );
131✔
UNCOV
498
        } catch (\UaRequest\Exception\NotFoundException) {
×
499
            $platformCodes = [\BrowserDetector\Data\Os::unknown];
×
500
        }
501

502
        $firstPlatformCode      = array_first($platformCodes);
131✔
503
        $platformCodeFromDevice = null;
131✔
504

505
        try {
506
            $platformCodeFromDevice = \BrowserDetector\Data\Os::fromName(
131✔
507
                (string) $platformCodenameFromDevice,
131✔
508
            );
131✔
UNCOV
509
        } catch (UnexpectedValueException) {
×
510
            // do nothing
511
        }
512

513
        if (
514
            (
515
                $firstPlatformCode instanceof \UaData\OsInterface
131✔
516
                && $firstPlatformCode === \BrowserDetector\Data\Os::unknown
131✔
517
            )
518
            || $firstPlatformCode === null
131✔
519
        ) {
520
            $firstPlatformCode = $platformCodeFromDevice;
2✔
521
        }
522

523
        if ($firstPlatformCode instanceof \UaData\OsInterface) {
131✔
524
            $platform       = $firstPlatformCode;
131✔
525
            $platformHeader = array_first($headersWithPlatformCode);
131✔
526

527
            switch ($firstPlatformCode) {
528
                case \BrowserDetector\Data\Os::linux:
131✔
529
                    $lastPlatformCode = array_last($platformCodes);
19✔
530

531
                    if (
532
                        $lastPlatformCode instanceof \UaData\OsInterface
19✔
533
                        && $lastPlatformCode !== $firstPlatformCode
19✔
534
                        && $lastPlatformCode !== \BrowserDetector\Data\Os::unknown
19✔
535
                    ) {
536
                        $platform       = $lastPlatformCode;
7✔
537
                        $platformHeader = array_last($headersWithPlatformCode);
7✔
538
                    } else {
539
                        $platformHeader = array_first($headersWithPlatformCode);
12✔
540
                    }
541

542
                    break;
19✔
543
                case \BrowserDetector\Data\Os::macosx:
112✔
544
                    $lastPlatformCode = array_last($platformCodes);
7✔
545

546
                    if (
547
                        $lastPlatformCode instanceof \UaData\OsInterface
7✔
548
                        && $lastPlatformCode === \BrowserDetector\Data\Os::ios
7✔
549
                    ) {
550
                        $platform       = $lastPlatformCode;
2✔
551
                        $platformHeader = array_last($headersWithPlatformCode);
2✔
552
                    } else {
553
                        $platformHeader = array_first($headersWithPlatformCode);
5✔
554
                    }
555

556
                    break;
7✔
557
                case \BrowserDetector\Data\Os::windows:
105✔
558
                    $lastPlatformCode = array_last($platformCodes);
19✔
559
                    $platformHeader   = array_first($headersWithPlatformCode);
19✔
560

561
                    $headersWithPlatformVersion = array_filter(
19✔
562
                        $this->headers,
19✔
563
                        static fn (HeaderInterface $header): bool => $header->hasPlatformVersion(),
19✔
564
                    );
19✔
565

566
                    if (
567
                        $lastPlatformCode instanceof \UaData\OsInterface
19✔
568
                        && !array_key_exists('sec-ch-ua-platform-version', $headersWithPlatformVersion)
19✔
569
                        && in_array(
19✔
570
                            $lastPlatformCode,
19✔
571
                            [\BrowserDetector\Data\Os::windows10, \BrowserDetector\Data\Os::windowsnt62, \BrowserDetector\Data\Os::windowsnt61, \BrowserDetector\Data\Os::windowsnt],
19✔
572
                            true,
19✔
573
                        )
19✔
574
                    ) {
575
                        $platform       = $lastPlatformCode;
7✔
576
                        $platformHeader = array_last($headersWithPlatformCode);
7✔
577
                    }
578

579
                    break;
19✔
580
                case \BrowserDetector\Data\Os::android:
86✔
581
                    $lastPlatformCode = array_last($platformCodes);
74✔
582

583
                    if (
584
                        $lastPlatformCode instanceof \UaData\OsInterface
74✔
585
                        && in_array(
74✔
586
                            $lastPlatformCode,
74✔
587
                            [\BrowserDetector\Data\Os::fireos, \BrowserDetector\Data\Os::harmonyos],
74✔
588
                            true,
74✔
589
                        )
74✔
590
                    ) {
591
                        $platform       = $lastPlatformCode;
4✔
592
                        $platformHeader = array_last($headersWithPlatformCode);
4✔
593
                    } elseif ($platformCodeFromDevice === \BrowserDetector\Data\Os::fireos) {
70✔
594
                        $platform       = $platformCodeFromDevice;
1✔
595
                        $platformHeader = null;
1✔
596
                    } else {
597
                        $platformHeader = array_first($headersWithPlatformCode);
69✔
598
                    }
599

600
                    break;
74✔
601
                default:
602
                    // do nothing
603
                    break;
12✔
604
            }
605

606
            assert($platformHeader instanceof HeaderInterface || $platformHeader === null);
131✔
607

608
            $platformVersion = match ($platform) {
131✔
609
                \BrowserDetector\Data\Os::lineageos => $this->getVersionForLineageOs(),
131✔
610
                \BrowserDetector\Data\Os::fireos => $this->getVersionForFireOs(),
130✔
611
                \BrowserDetector\Data\Os::windows => $this->getVersionForWindows(),
126✔
612
                default => $this->getVersionForGeneric($platform, $platformHeader),
114✔
613
            };
131✔
614

615
            if ($platform === \BrowserDetector\Data\Os::windows) {
131✔
616
                assert($platformVersion instanceof VersionInterface);
12✔
617

618
                try {
619
                    $platform = match ($platformVersion->getVersion(VersionInterface::IGNORE_MICRO)) {
12✔
620
                        '11.0' => \BrowserDetector\Data\Os::windows11,
4✔
621
                        '10.0' => \BrowserDetector\Data\Os::windows10,
1✔
622
                        '8.1' => \BrowserDetector\Data\Os::windowsnt63,
1✔
623
                        '8.0' => \BrowserDetector\Data\Os::windowsnt62,
1✔
624
                        '7.0' => \BrowserDetector\Data\Os::windowsnt61,
2✔
625
                        '6.0' => \BrowserDetector\Data\Os::windowsnt60,
1✔
626
                        default => \BrowserDetector\Data\Os::windows,
2✔
627
                    };
12✔
UNCOV
628
                } catch (UnexpectedValueException) {
×
629
                    // do nothing here
630
                }
631
            }
632

633
            if ($platform !== \BrowserDetector\Data\Os::unknown) {
131✔
634
                try {
635
                    $platformFromOs = $this->platformLoader->loadFromOs(
130✔
636
                        os: $platform,
130✔
637
                        useragent: $platformHeader instanceof HeaderInterface ? $platformHeader->getValue() : '',
130✔
638
                    );
130✔
639

640
                    if (
641
                        $platformVersion instanceof VersionInterface
130✔
642
                        && ($platformVersion instanceof ForcedNullVersion || $platformVersion->getVersion() !== null)
130✔
643
                    ) {
644
                        return $platformFromOs->withVersion($platformVersion);
108✔
645
                    }
646

647
                    return $platformFromOs;
22✔
UNCOV
648
                } catch (UnexpectedValueException $e) {
×
649
                    $this->logger->info($e);
×
650
                }
651
            }
652
        }
653

654
        return new Os(
1✔
655
            name: null,
1✔
656
            marketingName: null,
1✔
657
            manufacturer: new Company(type: 'unknown', name: null, brandname: null),
1✔
658
            version: new NullVersion(),
1✔
659
            bits: Bits::unknown,
1✔
660
        );
1✔
661
    }
662

663
    /**
664
     * detect the device data
665
     *
666
     * @throws void
667
     */
668
    public function getDeviceData(): DeviceDataInterface
131✔
669
    {
670
        $headersWithDeviceCode = array_filter(
131✔
671
            $this->headers,
131✔
672
            static fn (HeaderInterface $header): bool => $header->hasDeviceCode(),
131✔
673
        );
131✔
674

675
        $deviceHeader   = array_first($headersWithDeviceCode);
131✔
676
        $deviceCodename = null;
131✔
677

678
        if ($deviceHeader instanceof HeaderInterface) {
131✔
679
            $deviceCodename = $deviceHeader->getDeviceCode();
130✔
680
        }
681

682
        if ($deviceCodename !== null) {
131✔
683
            [$company, $key] = explode('=', $deviceCodename, 2);
130✔
684

685
            try {
686
                $deviceLoader = ($this->deviceLoaderFactory)($company);
130✔
687

688
                return $deviceLoader->load($key);
130✔
UNCOV
689
            } catch (NotFoundException $e) {
×
690
                $this->logger->info(
×
691
                    new UnexpectedValueException(
×
692
                        sprintf('Device "%s" of Manufacturer "%s" was not found', $key, $company),
×
693
                        0,
×
694
                        $e,
×
695
                    ),
×
696
                );
×
697
            }
698
        }
699

700
        return new DeviceData(
1✔
701
            device: new Device(
1✔
702
                architecture: Architecture::unknown,
1✔
703
                deviceName: null,
1✔
704
                marketingName: null,
1✔
705
                manufacturer: new Company(type: 'unknown', name: null, brandname: null),
1✔
706
                brand: new Company(type: 'unknown', name: null, brandname: null),
1✔
707
                type: Type::Unknown,
1✔
708
                display: new Display(),
1✔
709
                dualOrientation: null,
1✔
710
                simCount: null,
1✔
711
                bits: Bits::unknown,
1✔
712
            ),
1✔
713
            os: null,
1✔
714
        );
1✔
715
    }
716

717
    /** @throws void */
718
    private function getEngineVersion(\UaData\EngineInterface $engine): VersionInterface
131✔
719
    {
720
        $headersWithEngineVersion = array_filter(
131✔
721
            $this->headers,
131✔
722
            static fn (HeaderInterface $header): bool => $header->hasEngineVersion(),
131✔
723
        );
131✔
724

725
        $engineVersionHeader = array_first($headersWithEngineVersion);
131✔
726

727
        if ($engineVersionHeader instanceof HeaderInterface) {
131✔
728
            return $engineVersionHeader->getEngineVersionWithEngine($engine);
130✔
729
        }
730

731
        return new NullVersion();
1✔
732
    }
733

734
    /**
735
     * @return array<string, VersionInterface>
736
     *
737
     * @throws void
738
     */
739
    private function getClientVersions(string | null $clientCodename): array
130✔
740
    {
741
        $headersWithClientVersion = array_filter(
130✔
742
            $this->headers,
130✔
743
            static fn (HeaderInterface $header): bool => $header->hasClientVersion(),
130✔
744
        );
130✔
745

746
        return array_map(
130✔
747
            static fn (HeaderInterface $clientVersionHeader): VersionInterface => $clientVersionHeader->getClientVersion(
130✔
748
                $clientCodename,
130✔
749
            ),
130✔
750
            $headersWithClientVersion,
130✔
751
        );
130✔
752
    }
753

754
    /** @throws VersionContainsDerivateException */
755
    private function getPlatformVersion(\UaData\OsInterface $platform): VersionInterface
119✔
756
    {
757
        $headersWithPlatformVersion = array_filter(
119✔
758
            $this->headers,
119✔
759
            static fn (HeaderInterface $header): bool => $header->hasPlatformVersion(),
119✔
760
        );
119✔
761

762
        $platformHeaderVersion = array_first($headersWithPlatformVersion);
119✔
763

764
        if ($platformHeaderVersion instanceof HeaderInterface) {
119✔
765
            return $platformHeaderVersion->getPlatformVersionWithOs($platform);
118✔
766
        }
767

768
        return new NullVersion();
1✔
769
    }
770

771
    /** @throws void */
772
    private function getVersionForLineageOs(): VersionInterface
1✔
773
    {
774
        try {
775
            $androidVersion = $this->getPlatformVersion(\BrowserDetector\Data\Os::android)->getVersion(
1✔
776
                VersionInterface::IGNORE_MINOR_IF_EMPTY,
1✔
777
            );
1✔
778

779
            $lineageOsVersion = new LineageOs(new VersionBuilder());
1✔
780

781
            return $lineageOsVersion->getVersion($androidVersion ?? '');
1✔
UNCOV
782
        } catch (VersionContainsDerivateException | UnexpectedValueException | NotNumericException) {
×
783
            // do nothing
784
        }
785

UNCOV
786
        return new NullVersion();
×
787
    }
788

789
    /** @throws void */
790
    private function getVersionForFireOs(): VersionInterface
4✔
791
    {
792
        try {
793
            $androidVersion = $this->getPlatformVersion(\BrowserDetector\Data\Os::android)->getVersion(
4✔
794
                VersionInterface::IGNORE_MINOR_IF_EMPTY,
4✔
795
            );
4✔
796

797
            $fireOsVersion = new FireOs(new VersionBuilder());
4✔
798

799
            return $fireOsVersion->getVersion($androidVersion ?? '');
4✔
UNCOV
800
        } catch (VersionContainsDerivateException | UnexpectedValueException | NotNumericException) {
×
801
            // do nothing
802
        }
803

UNCOV
804
        return new NullVersion();
×
805
    }
806

807
    /** @throws void */
808
    private function getVersionForGeneric(
114✔
809
        \UaData\OsInterface &$platform,
810
        HeaderInterface | null $platformHeader,
811
    ): VersionInterface {
812
        try {
813
            return $this->getPlatformVersion($platform);
114✔
814
        } catch (VersionContainsDerivateException $e) {
1✔
815
            $derivate = $e->getDerivate();
1✔
816

817
            if ($platformHeader instanceof HeaderInterface && $derivate !== '') {
1✔
818
                try {
819
                    $derivateOs = $platformHeader->getPlatformCode($derivate);
1✔
820

821
                    if ($derivateOs !== \BrowserDetector\Data\Os::unknown) {
1✔
822
                        $platform = $derivateOs;
1✔
823
                    }
UNCOV
824
                } catch (\UaRequest\Exception\NotFoundException) {
×
825
                    // do nothing
826
                }
827
            }
828
        }
829

830
        return new NullVersion();
1✔
831
    }
832

833
    /** @throws void */
834
    private function getVersionForWindows(): VersionInterface
12✔
835
    {
836
        $platform = \BrowserDetector\Data\Os::windows;
12✔
837

838
        $headersWithPlatformVersion = array_filter(
12✔
839
            $this->headers,
12✔
840
            static fn (HeaderInterface $header): bool => $header->hasPlatformVersion(),
12✔
841
        );
12✔
842

843
        if (array_key_exists('sec-ch-ua-platform-version', $headersWithPlatformVersion)) {
12✔
844
            $platformHeaderVersion = $headersWithPlatformVersion['sec-ch-ua-platform-version'];
9✔
845
        } else {
846
            $platformHeaderVersion = array_last($headersWithPlatformVersion);
3✔
847

848
            $platform = \BrowserDetector\Data\Os::unknown;
3✔
849
        }
850

851
        if ($platformHeaderVersion instanceof HeaderInterface) {
12✔
852
            return $platformHeaderVersion->getPlatformVersionWithOs($platform);
12✔
853
        }
854

UNCOV
855
        return new NullVersion();
×
856
    }
857
}
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