$this->parseBrowser($userAgent), 'browser_version' => $this->parseBrowserVersion($userAgent), 'operating_system' => $this->parseOs($userAgent), 'operating_system_version' => $this->parseOsVersion($userAgent), 'device_type' => $this->parseDeviceType($userAgent), ]; } private function parseBrowser(string $ua): ?string { // Order matters — check specific first, generic last $patterns = [ 'Edg/' => 'Edge', 'EdgiOS' => 'Edge', 'EdgA' => 'Edge', 'OPR/' => 'Opera', 'Opera' => 'Opera', 'SamsungBrowser' => 'Samsung Browser', 'Chrome' => 'Chrome', 'CriOS' => 'Chrome', // Chrome iOS 'Firefox' => 'Firefox', 'FxiOS' => 'Firefox', // Firefox iOS 'Safari' => 'Safari', 'MSIE' => 'Internet Explorer', 'Trident' => 'Internet Explorer', 'Googlebot' => 'Googlebot', 'bingbot' => 'Bingbot', 'DuckDuckBot' => 'DuckDuckBot', 'Slurp' => 'Yahoo! Slurp', 'curl' => 'cURL', ]; foreach ($patterns as $token => $name) { if (stripos($ua, $token) !== false) { return $name; } } return null; } private function parseBrowserVersion(string $ua): ?string { $patterns = [ '/Edg\/([0-9.]+)/', '/EdgiOS\/([0-9.]+)/', '/EdgA\/([0-9.]+)/', '/OPR\/([0-9.]+)/', '/SamsungBrowser\/([0-9.]+)/', '/CriOS\/([0-9.]+)/', '/Chrome\/([0-9.]+)/', '/FxiOS\/([0-9.]+)/', '/Firefox\/([0-9.]+)/', '/Version\/([0-9.]+).*Safari/', '/MSIE ([0-9.]+)/', '/rv:([0-9.]+).*Trident/', ]; foreach ($patterns as $pattern) { if (preg_match($pattern, $ua, $matches)) { // Return major.minor only $parts = explode('.', $matches[1]); return implode('.', array_slice($parts, 0, 2)); } } return null; } private function parseOs(string $ua): ?string { return match (true) { stripos($ua, 'Windows') !== false => 'Windows', stripos($ua, 'iPad') !== false => 'iPadOS', stripos($ua, 'iPhone') !== false => 'iOS', stripos($ua, 'Mac OS X') !== false => 'macOS', stripos($ua, 'Android') !== false => 'Android', stripos($ua, 'Linux') !== false => 'Linux', stripos($ua, 'CrOS') !== false => 'Chrome OS', default => null, }; } private function parseOsVersion(string $ua): ?string { // Windows: Windows NT 10.0 if (preg_match('/Windows NT ([0-9.]+)/', $ua, $m)) { return $this->windowsVersion($m[1]); } // Android: Android 13 if (preg_match('/Android ([0-9.]+)/', $ua, $m)) { return $m[1]; } // iOS / iPadOS: OS 17_0 if (preg_match('/OS ([0-9_]+) like Mac/', $ua, $m)) { return str_replace('_', '.', $m[1]); } // macOS: Mac OS X 10_15_7 or 14.0 if (preg_match('/Mac OS X ([0-9_.]+)/', $ua, $m)) { return str_replace('_', '.', $m[1]); } // Linux — usually no version if (preg_match('/Linux ([0-9.]+)/', $ua, $m)) { return $m[1]; } return null; } private function parseDeviceType(string $ua): string { // Robots first $robots = ['Googlebot', 'bingbot', 'Slurp', 'DuckDuckBot', 'Baiduspider', 'YandexBot', 'facebookexternalhit', 'Twitterbot', 'Applebot', 'curl', 'python-requests', 'Go-http-client']; foreach ($robots as $robot) { if (stripos($ua, $robot) !== false) { return 'robot'; } } // Tablets before phones — iPad + Android tablets if ( stripos($ua, 'iPad') !== false || (stripos($ua, 'Android') !== false && stripos($ua, 'Mobile') === false) ) { return 'tablet'; } // Mobile phones $mobileKeywords = ['Mobile', 'iPhone', 'iPod', 'BlackBerry', 'IEMobile', 'Opera Mini', 'SamsungBrowser', 'Kindle']; foreach ($mobileKeywords as $keyword) { if (stripos($ua, $keyword) !== false) { return 'mobile'; } } return 'desktop'; } private function windowsVersion(string $ntVersion): string { return match ($ntVersion) { '10.0' => '10/11', '6.3' => '8.1', '6.2' => '8', '6.1' => '7', '6.0' => 'Vista', '5.1' => 'XP', default => $ntVersion, }; } }