Files
michaelschiemer/src/Framework/UserAgent/UserAgentParser.php
Michael Schiemer 55a330b223 Enable Discovery debug logging for production troubleshooting
- Add DISCOVERY_LOG_LEVEL=debug
- Add DISCOVERY_SHOW_PROGRESS=true
- Temporary changes for debugging InitializerProcessor fixes on production
2025-08-11 20:13:26 +02:00

255 lines
7.6 KiB
PHP

<?php
declare(strict_types=1);
namespace App\Framework\UserAgent;
use App\Framework\Cache\Cache;
use App\Framework\UserAgent\Enums\BrowserType;
use App\Framework\UserAgent\Enums\EngineType;
use App\Framework\UserAgent\Enums\PlatformType;
use App\Framework\UserAgent\Patterns\BrowserPatterns;
use App\Framework\UserAgent\Patterns\EnginePatterns;
use App\Framework\UserAgent\Patterns\PlatformPatterns;
/**
* High-performance User-Agent parser with caching
* Optimized for modern browsers and frameworks
*/
final class UserAgentParser
{
public function __construct(
private readonly ?Cache $cache = null
) {
}
/**
* Parse User-Agent string into structured ParsedUserAgent object
*/
public function parse(string $userAgentString): ParsedUserAgent
{
$normalized = trim($userAgentString);
// Handle empty User-Agent
if ($normalized === '') {
return $this->createUnknownUserAgent('');
}
// Check cache first
$cacheKey = 'useragent:' . md5($normalized);
if ($this->cache) {
$cached = $this->cache->get($cacheKey);
if ($cached instanceof ParsedUserAgent) {
return $cached;
}
}
// Parse components
$browser = $this->parseBrowser($normalized);
$browserVersion = $this->parseBrowserVersion($normalized, $browser);
$platform = $this->parsePlatform($normalized);
$platformVersion = $this->parsePlatformVersion($normalized, $platform);
$engine = $this->parseEngine($normalized, $browser);
$engineVersion = $this->parseEngineVersion($normalized, $engine);
$isMobile = PlatformPatterns::isMobile($normalized) || $platform->isMobile();
$isBot = BrowserPatterns::isBot($normalized);
$isModern = $this->determineModernBrowser($browser, $browserVersion, $isBot);
$parsedUserAgent = new ParsedUserAgent(
raw: $normalized,
browser: $browser,
browserVersion: $browserVersion,
platform: $platform,
platformVersion: $platformVersion,
engine: $engine,
engineVersion: $engineVersion,
isMobile: $isMobile,
isBot: $isBot,
isModern: $isModern
);
// Cache result
if ($this->cache) {
$this->cache->set($cacheKey, $parsedUserAgent, 3600); // Cache for 1 hour
}
return $parsedUserAgent;
}
/**
* Parse browser type and version
*/
private function parseBrowser(string $userAgent): BrowserType
{
foreach (BrowserPatterns::getPatterns() as $pattern) {
if (preg_match($pattern['pattern'], $userAgent)) {
return $pattern['browser'];
}
}
// Try fallback patterns
foreach (BrowserPatterns::getFallbackPatterns() as $fallback) {
if (preg_match($fallback['pattern'], $userAgent)) {
return $fallback['browser'];
}
}
return BrowserType::UNKNOWN;
}
/**
* Parse browser version
*/
private function parseBrowserVersion(string $userAgent, BrowserType $browser): string
{
// Find matching pattern for this browser
foreach (BrowserPatterns::getPatterns() as $pattern) {
if ($pattern['browser'] === $browser && preg_match($pattern['versionPattern'], $userAgent, $matches)) {
return $matches[1] ?? 'Unknown';
}
}
return 'Unknown';
}
/**
* Parse platform/operating system
*/
private function parsePlatform(string $userAgent): PlatformType
{
foreach (PlatformPatterns::getPatterns() as $pattern) {
if (preg_match($pattern['pattern'], $userAgent)) {
return $pattern['platform'];
}
}
return PlatformType::UNKNOWN;
}
/**
* Parse platform version
*/
private function parsePlatformVersion(string $userAgent, PlatformType $platform): string
{
foreach (PlatformPatterns::getPatterns() as $pattern) {
if ($pattern['platform'] === $platform &&
! empty($pattern['versionPattern']) &&
preg_match($pattern['versionPattern'], $userAgent, $matches)) {
$version = $matches[1] ?? 'Unknown';
// Format version based on platform
return match ($platform) {
PlatformType::WINDOWS => PlatformPatterns::formatWindowsVersion($version),
PlatformType::MACOS, PlatformType::IOS => PlatformPatterns::formatAppleVersion($version),
default => $version
};
}
}
return 'Unknown';
}
/**
* Parse browser engine
*/
private function parseEngine(string $userAgent, BrowserType $browser): EngineType
{
// Try pattern-based detection first
foreach (EnginePatterns::getPatterns() as $pattern) {
if (preg_match($pattern['pattern'], $userAgent)) {
return $pattern['engine'];
}
}
// Fallback to browser-based mapping
$engineMap = EnginePatterns::getBrowserEngineMap();
return $engineMap[$browser->value] ?? EngineType::UNKNOWN;
}
/**
* Parse engine version
*/
private function parseEngineVersion(string $userAgent, EngineType $engine): string
{
foreach (EnginePatterns::getPatterns() as $pattern) {
if ($pattern['engine'] === $engine && preg_match($pattern['versionPattern'], $userAgent, $matches)) {
$version = $matches[1] ?? 'Unknown';
// Special formatting for Gecko
if ($engine === EngineType::GECKO) {
return EnginePatterns::formatGeckoVersion($version);
}
return $version;
}
}
return 'Unknown';
}
/**
* Determine if browser is considered modern
*/
private function determineModernBrowser(BrowserType $browser, string $version, bool $isBot): bool
{
if ($isBot || $version === 'Unknown') {
return false;
}
if (! $browser->isModern()) {
return false;
}
$threshold = $browser->getModernVersionThreshold();
return version_compare($version, $threshold, '>=');
}
/**
* Create unknown ParsedUserAgent object
*/
private function createUnknownUserAgent(string $raw): ParsedUserAgent
{
return new ParsedUserAgent(
raw: $raw,
browser: BrowserType::UNKNOWN,
browserVersion: 'Unknown',
platform: PlatformType::UNKNOWN,
platformVersion: 'Unknown',
engine: EngineType::UNKNOWN,
engineVersion: 'Unknown',
isMobile: false,
isBot: false,
isModern: false
);
}
/**
* Clear parser cache
*/
public function clearCache(): void
{
if ($this->cache) {
// Clear all useragent cache entries
// This would need cache implementation that supports key patterns
// For now, this is a placeholder
}
}
/**
* Get parser statistics (for debugging/monitoring)
* @return array<string, mixed>
*/
public function getStats(): array
{
return [
'cacheEnabled' => $this->cache !== null,
'supportedBrowsers' => count(BrowserType::cases()),
'supportedPlatforms' => count(PlatformType::cases()),
'supportedEngines' => count(EngineType::cases()),
];
}
}