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

mimmi20 / browser-detector / 21861966519

10 Feb 2026 05:34AM UTC coverage: 95.809% (-0.1%) from 95.958%
21861966519

push

github

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

add new client

289 of 319 new or added lines in 21 files covered. (90.6%)

1 existing line in 1 file now uncovered.

12552 of 13101 relevant lines covered (95.81%)

64.22 hits per line

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

93.8
/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(
132✔
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();
132✔
76
    }
77

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

145
        return null;
111✔
146
    }
147

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

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

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

162
        return null;
55✔
163
    }
164

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

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

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

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

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

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

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

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

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

220
        return new Engine(
2✔
221
            name: null,
2✔
222
            manufacturer: new Company(type: 'unknown', name: null, brandname: null),
2✔
223
            version: $engineVersion,
2✔
224
        );
2✔
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
132✔
235
    {
236
        $headersWithClientCode = array_filter(
132✔
237
            $this->headers,
132✔
238
            static fn (HeaderInterface $header): bool => $header->hasClientCode(),
132✔
239
        );
132✔
240

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

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

250
        if (is_string($firstClientCodename)) {
132✔
251
            switch ($firstClientCodename) {
252
                case 'android webview':
131✔
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':
128✔
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':
106✔
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':
80✔
352
                    $lastClientCodename = array_last($clientCodes);
10✔
353
                    $clientHeader       = array_first($headersWithClientCode);
10✔
354

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

367
                                break;
7✔
368
                            default:
369
                                // do nothing
370
                        }
371
                    }
372

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

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

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

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

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

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

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

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

424
                    break;
53✔
425
            }
426

427
            assert($clientHeader instanceof HeaderInterface);
131✔
428

429
            $clientVersions = $this->getClientVersions($clientCodename);
131✔
430
            $clientVersion  = match ($clientCodename) {
131✔
431
                'aloha-browser', 'opera touch', 'adblock browser', 'opera mini', 'baidu box app lite', 'opera', 'silk', 'mint browser', 'instagram app', 'bingsearch', 'stargon-browser', 'yahoo-japan-app', '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', 'meta-external-agent' => $clientVersions['user-agent']
62✔
432
                    ?? array_last($clientVersions),
62✔
433
                'duckduck app', 'ucbrowser', 'edge', 'headless-chrome' => $clientVersions['sec-ch-ua-full-version-list']
13✔
434
                    ?? $clientVersions['sec-ch-ua']
13✔
435
                    ?? $clientVersions['sec-ch-ua-full-version']
13✔
436
                    ?? array_last($clientVersions),
13✔
437
                'ecosia' => $clientVersions['sec-ch-ua-full-version']
1✔
438
                    ?? array_last($clientVersions),
1✔
439
                'vivaldi' => $clientVersions['sec-ch-ua']
1✔
440
                    ?? array_last($clientVersions),
1✔
441
                'chrome' => $chromeClientVersion
18✔
442
                    ?? $clientVersions['sec-ch-ua-full-version-list']
18✔
443
                    ?? $clientVersions['sec-ch-ua']
18✔
444
                    ?? $clientVersions['sec-ch-ua-full-version']
18✔
445
                    ?? array_last($clientVersions),
18✔
446
                default => array_first($clientVersions),
36✔
447
            };
131✔
448

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

455
                if ($clientVersion?->getVersion() !== null) {
131✔
456
                    $client = $clientData->getClient();
111✔
457

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

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

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

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

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

503
        $firstPlatformCode      = array_first($platformCodes);
132✔
504
        $platformCodeFromDevice = null;
132✔
505

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

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

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

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

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

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

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

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

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

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

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

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

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

607
            assert($platformHeader instanceof HeaderInterface || $platformHeader === null);
132✔
608

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

616
            if ($platform === \BrowserDetector\Data\Os::windows) {
132✔
617
                assert($platformVersion instanceof VersionInterface);
13✔
618

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

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

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

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

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

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

676
        $deviceHeader   = array_first($headersWithDeviceCode);
132✔
677
        $deviceCodename = null;
132✔
678

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

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

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

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

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

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

726
        $engineVersionHeader = array_first($headersWithEngineVersion);
132✔
727

728
        if ($engineVersionHeader instanceof HeaderInterface) {
132✔
729
            return $engineVersionHeader->getEngineVersionWithEngine($engine);
132✔
730
        }
731

UNCOV
732
        return new NullVersion();
×
733
    }
734

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

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

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

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

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

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

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

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

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

787
        return new NullVersion();
×
788
    }
789

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

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

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

805
        return new NullVersion();
×
806
    }
807

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

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

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

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

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

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

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

849
            $platform = \BrowserDetector\Data\Os::unknown;
4✔
850
        }
851

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

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