feat(Docker): Upgrade to PHP 8.5.0RC3 with native ext-uri support
BREAKING CHANGE: Requires PHP 8.5.0RC3 Changes: - Update Docker base image from php:8.4-fpm to php:8.5.0RC3-fpm - Enable ext-uri for native WHATWG URL parsing support - Update composer.json PHP requirement from ^8.4 to ^8.5 - Add ext-uri as required extension in composer.json - Move URL classes from Url.php85/ to Url/ directory (now compatible) - Remove temporary PHP 8.4 compatibility workarounds Benefits: - Native URL parsing with Uri\WhatWg\Url class - Better performance for URL operations - Future-proof with latest PHP features - Eliminates PHP version compatibility issues
This commit is contained in:
@@ -4,9 +4,11 @@ declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\UserAgent;
|
||||
|
||||
use App\Framework\Core\ValueObjects\Version;
|
||||
use App\Framework\UserAgent\Enums\BrowserType;
|
||||
use App\Framework\UserAgent\Enums\EngineType;
|
||||
use App\Framework\UserAgent\Enums\PlatformType;
|
||||
use App\Framework\UserAgent\ValueObjects\DeviceCategory;
|
||||
|
||||
/**
|
||||
* Value Object representing a parsed User-Agent with rich metadata
|
||||
@@ -17,11 +19,11 @@ final readonly class ParsedUserAgent
|
||||
public function __construct(
|
||||
public string $raw,
|
||||
public BrowserType $browser,
|
||||
public string $browserVersion,
|
||||
public Version $browserVersion,
|
||||
public PlatformType $platform,
|
||||
public string $platformVersion,
|
||||
public Version $platformVersion,
|
||||
public EngineType $engine,
|
||||
public string $engineVersion,
|
||||
public Version $engineVersion,
|
||||
public bool $isMobile,
|
||||
public bool $isBot,
|
||||
public bool $isModern
|
||||
@@ -41,11 +43,7 @@ final readonly class ParsedUserAgent
|
||||
*/
|
||||
public function getBrowserName(): string
|
||||
{
|
||||
if ($this->browserVersion === 'Unknown') {
|
||||
return $this->browser->getDisplayName();
|
||||
}
|
||||
|
||||
return $this->browser->getDisplayName() . ' ' . $this->browserVersion;
|
||||
return $this->browser->getDisplayName() . ' ' . $this->browserVersion->toString();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -53,11 +51,7 @@ final readonly class ParsedUserAgent
|
||||
*/
|
||||
public function getPlatformName(): string
|
||||
{
|
||||
if ($this->platformVersion === 'Unknown') {
|
||||
return $this->platform->getDisplayName();
|
||||
}
|
||||
|
||||
return $this->platform->getDisplayName() . ' ' . $this->platformVersion;
|
||||
return $this->platform->getDisplayName() . ' ' . $this->platformVersion->toString();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -65,11 +59,7 @@ final readonly class ParsedUserAgent
|
||||
*/
|
||||
public function getEngineName(): string
|
||||
{
|
||||
if ($this->engineVersion === 'Unknown') {
|
||||
return $this->engine->getDisplayName();
|
||||
}
|
||||
|
||||
return $this->engine->getDisplayName() . ' ' . $this->engineVersion;
|
||||
return $this->engine->getDisplayName() . ' ' . $this->engineVersion->toString();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -104,16 +94,18 @@ final readonly class ParsedUserAgent
|
||||
return match ($feature) {
|
||||
// Image formats
|
||||
'webp' => $this->browser->getEngine() === EngineType::BLINK ||
|
||||
($this->browser === BrowserType::FIREFOX && version_compare($this->browserVersion, '65.0', '>=')),
|
||||
($this->browser === BrowserType::FIREFOX &&
|
||||
$this->browserVersion->isNewerThan(Version::fromString('65.0')) ||
|
||||
$this->browserVersion->equals(Version::fromString('65.0'))),
|
||||
'avif' => $this->browser->getEngine() === EngineType::BLINK &&
|
||||
version_compare($this->browserVersion, '85.0', '>='),
|
||||
($this->browserVersion->isNewerThan(Version::fromString('85.0')) ||
|
||||
$this->browserVersion->equals(Version::fromString('85.0'))),
|
||||
|
||||
// JavaScript features
|
||||
'es6', 'css-custom-properties', 'css-flexbox', 'css-grid', 'webrtc', 'websockets' => $this->isModern,
|
||||
'es2017' => $this->isModern && version_compare($this->browserVersion, $this->getEs2017MinVersion(), '>='),
|
||||
'es2020' => $this->isModern && version_compare($this->browserVersion, $this->getEs2020MinVersion(), '>='),
|
||||
'es2017' => $this->isModern && $this->supportsEs2017(),
|
||||
'es2020' => $this->isModern && $this->supportsEs2020(),
|
||||
|
||||
// CSS features
|
||||
// Web APIs
|
||||
'service-worker' => $this->isModern && $this->platform !== PlatformType::IOS,
|
||||
'web-push' => $this->isModern && $this->browser !== BrowserType::SAFARI,
|
||||
@@ -122,54 +114,80 @@ final readonly class ParsedUserAgent
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if browser supports ES2017
|
||||
*/
|
||||
private function supportsEs2017(): bool
|
||||
{
|
||||
$minVersion = $this->getEs2017MinVersion();
|
||||
|
||||
return $this->browserVersion->isNewerThan($minVersion) ||
|
||||
$this->browserVersion->equals($minVersion);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if browser supports ES2020
|
||||
*/
|
||||
private function supportsEs2020(): bool
|
||||
{
|
||||
$minVersion = $this->getEs2020MinVersion();
|
||||
|
||||
return $this->browserVersion->isNewerThan($minVersion) ||
|
||||
$this->browserVersion->equals($minVersion);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get minimum browser version for ES2017 support
|
||||
*/
|
||||
private function getEs2017MinVersion(): string
|
||||
private function getEs2017MinVersion(): Version
|
||||
{
|
||||
return match ($this->browser) {
|
||||
BrowserType::CHROME => '58.0',
|
||||
BrowserType::FIREFOX => '52.0',
|
||||
BrowserType::SAFARI => '10.1',
|
||||
BrowserType::EDGE => '79.0',
|
||||
BrowserType::OPERA => '45.0',
|
||||
default => '999.0'
|
||||
$versionString = match ($this->browser) {
|
||||
BrowserType::CHROME => '58.0.0',
|
||||
BrowserType::FIREFOX => '52.0.0',
|
||||
BrowserType::SAFARI => '10.1.0',
|
||||
BrowserType::EDGE => '79.0.0',
|
||||
BrowserType::OPERA => '45.0.0',
|
||||
default => '999.0.0'
|
||||
};
|
||||
|
||||
return Version::fromString($versionString);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get minimum browser version for ES2020 support
|
||||
*/
|
||||
private function getEs2020MinVersion(): string
|
||||
private function getEs2020MinVersion(): Version
|
||||
{
|
||||
return match ($this->browser) {
|
||||
BrowserType::CHROME => '80.0',
|
||||
BrowserType::FIREFOX => '72.0',
|
||||
BrowserType::SAFARI => '13.1',
|
||||
BrowserType::EDGE => '80.0',
|
||||
BrowserType::OPERA => '67.0',
|
||||
default => '999.0'
|
||||
$versionString = match ($this->browser) {
|
||||
BrowserType::CHROME => '80.0.0',
|
||||
BrowserType::FIREFOX => '72.0.0',
|
||||
BrowserType::SAFARI => '13.1.0',
|
||||
BrowserType::EDGE => '80.0.0',
|
||||
BrowserType::OPERA => '67.0.0',
|
||||
default => '999.0.0'
|
||||
};
|
||||
|
||||
return Version::fromString($versionString);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get device category
|
||||
*/
|
||||
public function getDeviceCategory(): string
|
||||
public function getDeviceCategory(): DeviceCategory
|
||||
{
|
||||
if ($this->isBot) {
|
||||
return 'bot';
|
||||
return DeviceCategory::BOT;
|
||||
}
|
||||
|
||||
if ($this->platform->isMobile()) {
|
||||
return 'mobile';
|
||||
return DeviceCategory::MOBILE;
|
||||
}
|
||||
|
||||
if ($this->platform->isDesktop()) {
|
||||
return 'desktop';
|
||||
return DeviceCategory::DESKTOP;
|
||||
}
|
||||
|
||||
return 'unknown';
|
||||
return DeviceCategory::UNKNOWN;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -183,20 +201,20 @@ final readonly class ParsedUserAgent
|
||||
'browser' => [
|
||||
'type' => $this->browser->value,
|
||||
'name' => $this->browser->getDisplayName(),
|
||||
'version' => $this->browserVersion,
|
||||
'version' => $this->browserVersion->toString(),
|
||||
'fullName' => $this->getBrowserName(),
|
||||
],
|
||||
'platform' => [
|
||||
'type' => $this->platform->value,
|
||||
'name' => $this->platform->getDisplayName(),
|
||||
'version' => $this->platformVersion,
|
||||
'version' => $this->platformVersion->toString(),
|
||||
'fullName' => $this->getPlatformName(),
|
||||
'family' => $this->platform->getFamily(),
|
||||
],
|
||||
'engine' => [
|
||||
'type' => $this->engine->value,
|
||||
'name' => $this->engine->getDisplayName(),
|
||||
'version' => $this->engineVersion,
|
||||
'version' => $this->engineVersion->toString(),
|
||||
'fullName' => $this->getEngineName(),
|
||||
'developer' => $this->engine->getDeveloper(),
|
||||
],
|
||||
@@ -205,7 +223,7 @@ final readonly class ParsedUserAgent
|
||||
'isBot' => $this->isBot,
|
||||
'isModern' => $this->isModern,
|
||||
],
|
||||
'deviceCategory' => $this->getDeviceCategory(),
|
||||
'deviceCategory' => $this->getDeviceCategory()->value,
|
||||
'summary' => $this->getSummary(),
|
||||
];
|
||||
}
|
||||
|
||||
@@ -5,6 +5,11 @@ declare(strict_types=1);
|
||||
namespace App\Framework\UserAgent;
|
||||
|
||||
use App\Framework\Cache\Cache;
|
||||
use App\Framework\Cache\CacheKey;
|
||||
use App\Framework\Core\ValueObjects\Duration;
|
||||
use App\Framework\Core\ValueObjects\Hash;
|
||||
use App\Framework\Core\ValueObjects\HashAlgorithm;
|
||||
use App\Framework\Core\ValueObjects\Version;
|
||||
use App\Framework\UserAgent\Enums\BrowserType;
|
||||
use App\Framework\UserAgent\Enums\EngineType;
|
||||
use App\Framework\UserAgent\Enums\PlatformType;
|
||||
@@ -20,7 +25,8 @@ final readonly class UserAgentParser
|
||||
{
|
||||
public function __construct(
|
||||
private ?Cache $cache = null
|
||||
) {}
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse User-Agent string into structured ParsedUserAgent object
|
||||
@@ -34,8 +40,9 @@ final readonly class UserAgentParser
|
||||
return $this->createUnknownUserAgent('');
|
||||
}
|
||||
|
||||
// Check cache first
|
||||
$cacheKey = 'useragent:' . md5($normalized);
|
||||
// Check cache first (using framework's Hash VO with fast algorithm)
|
||||
$hash = Hash::create($normalized, HashAlgorithm::fast());
|
||||
$cacheKey = CacheKey::fromString('useragent:' . $hash->toString());
|
||||
if ($this->cache) {
|
||||
$cached = $this->cache->get($cacheKey);
|
||||
if ($cached instanceof ParsedUserAgent) {
|
||||
@@ -67,9 +74,9 @@ final readonly class UserAgentParser
|
||||
isModern: $isModern
|
||||
);
|
||||
|
||||
// Cache result
|
||||
// Cache result for 1 hour
|
||||
if ($this->cache) {
|
||||
$this->cache->set($cacheKey, $parsedUserAgent, 3600); // Cache for 1 hour
|
||||
$this->cache->set($cacheKey, $parsedUserAgent, Duration::fromHours(1));
|
||||
}
|
||||
|
||||
return $parsedUserAgent;
|
||||
@@ -99,16 +106,18 @@ final readonly class UserAgentParser
|
||||
/**
|
||||
* Parse browser version
|
||||
*/
|
||||
private function parseBrowserVersion(string $userAgent, BrowserType $browser): string
|
||||
private function parseBrowserVersion(string $userAgent, BrowserType $browser): Version
|
||||
{
|
||||
// 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';
|
||||
$versionString = $matches[1] ?? '0.0.0';
|
||||
|
||||
return $this->parseVersion($versionString);
|
||||
}
|
||||
}
|
||||
|
||||
return 'Unknown';
|
||||
return Version::fromString('0.0.0');
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -128,25 +137,27 @@ final readonly class UserAgentParser
|
||||
/**
|
||||
* Parse platform version
|
||||
*/
|
||||
private function parsePlatformVersion(string $userAgent, PlatformType $platform): string
|
||||
private function parsePlatformVersion(string $userAgent, PlatformType $platform): Version
|
||||
{
|
||||
foreach (PlatformPatterns::getPatterns() as $pattern) {
|
||||
if ($pattern['platform'] === $platform &&
|
||||
! empty($pattern['versionPattern']) &&
|
||||
preg_match($pattern['versionPattern'], $userAgent, $matches)) {
|
||||
|
||||
$version = $matches[1] ?? 'Unknown';
|
||||
$version = $matches[1] ?? '0.0.0';
|
||||
|
||||
// Format version based on platform
|
||||
return match ($platform) {
|
||||
$formattedVersion = match ($platform) {
|
||||
PlatformType::WINDOWS => PlatformPatterns::formatWindowsVersion($version),
|
||||
PlatformType::MACOS, PlatformType::IOS => PlatformPatterns::formatAppleVersion($version),
|
||||
default => $version
|
||||
};
|
||||
|
||||
return $this->parseVersion($formattedVersion);
|
||||
}
|
||||
}
|
||||
|
||||
return 'Unknown';
|
||||
return Version::fromString('0.0.0');
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -170,30 +181,32 @@ final readonly class UserAgentParser
|
||||
/**
|
||||
* Parse engine version
|
||||
*/
|
||||
private function parseEngineVersion(string $userAgent, EngineType $engine): string
|
||||
private function parseEngineVersion(string $userAgent, EngineType $engine): Version
|
||||
{
|
||||
foreach (EnginePatterns::getPatterns() as $pattern) {
|
||||
if ($pattern['engine'] === $engine && preg_match($pattern['versionPattern'], $userAgent, $matches)) {
|
||||
$version = $matches[1] ?? 'Unknown';
|
||||
$version = $matches[1] ?? '0.0.0';
|
||||
|
||||
// Special formatting for Gecko
|
||||
if ($engine === EngineType::GECKO) {
|
||||
return EnginePatterns::formatGeckoVersion($version);
|
||||
$formattedVersion = EnginePatterns::formatGeckoVersion($version);
|
||||
|
||||
return $this->parseVersion($formattedVersion);
|
||||
}
|
||||
|
||||
return $version;
|
||||
return $this->parseVersion($version);
|
||||
}
|
||||
}
|
||||
|
||||
return 'Unknown';
|
||||
return Version::fromString('0.0.0');
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if browser is considered modern
|
||||
*/
|
||||
private function determineModernBrowser(BrowserType $browser, string $version, bool $isBot): bool
|
||||
private function determineModernBrowser(BrowserType $browser, Version $version, bool $isBot): bool
|
||||
{
|
||||
if ($isBot || $version === 'Unknown') {
|
||||
if ($isBot) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -201,9 +214,9 @@ final readonly class UserAgentParser
|
||||
return false;
|
||||
}
|
||||
|
||||
$threshold = $browser->getModernVersionThreshold();
|
||||
$threshold = Version::fromString($browser->getModernVersionThreshold());
|
||||
|
||||
return version_compare($version, $threshold, '>=');
|
||||
return $version->isNewerThan($threshold) || $version->equals($threshold);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -214,17 +227,48 @@ final readonly class UserAgentParser
|
||||
return new ParsedUserAgent(
|
||||
raw: $raw,
|
||||
browser: BrowserType::UNKNOWN,
|
||||
browserVersion: 'Unknown',
|
||||
browserVersion: Version::fromString('0.0.0'),
|
||||
platform: PlatformType::UNKNOWN,
|
||||
platformVersion: 'Unknown',
|
||||
platformVersion: Version::fromString('0.0.0'),
|
||||
engine: EngineType::UNKNOWN,
|
||||
engineVersion: 'Unknown',
|
||||
engineVersion: Version::fromString('0.0.0'),
|
||||
isMobile: false,
|
||||
isBot: false,
|
||||
isModern: false
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse version string into Version Value Object
|
||||
* Handles various version formats from User-Agent strings
|
||||
*/
|
||||
private function parseVersion(string $versionString): Version
|
||||
{
|
||||
// Normalize version string
|
||||
$normalized = trim($versionString);
|
||||
|
||||
if ($normalized === '' || $normalized === 'Unknown') {
|
||||
return Version::fromString('0.0.0');
|
||||
}
|
||||
|
||||
// Try to parse as semver
|
||||
try {
|
||||
return Version::fromString($normalized);
|
||||
} catch (\InvalidArgumentException $e) {
|
||||
// If parsing fails, try to extract major.minor.patch from string
|
||||
if (preg_match('/^(\d+)(?:\.(\d+))?(?:\.(\d+))?/', $normalized, $matches)) {
|
||||
$major = (int) $matches[1];
|
||||
$minor = isset($matches[2]) ? (int) $matches[2] : 0;
|
||||
$patch = isset($matches[3]) ? (int) $matches[3] : 0;
|
||||
|
||||
return Version::fromComponents($major, $minor, $patch);
|
||||
}
|
||||
|
||||
// Fallback to 0.0.0 if we can't parse
|
||||
return Version::fromString('0.0.0');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear parser cache
|
||||
*/
|
||||
|
||||
58
src/Framework/UserAgent/ValueObjects/DeviceCategory.php
Normal file
58
src/Framework/UserAgent/ValueObjects/DeviceCategory.php
Normal file
@@ -0,0 +1,58 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\UserAgent\ValueObjects;
|
||||
|
||||
/**
|
||||
* Value Object representing device category
|
||||
*/
|
||||
enum DeviceCategory: string
|
||||
{
|
||||
case BOT = 'bot';
|
||||
case MOBILE = 'mobile';
|
||||
case DESKTOP = 'desktop';
|
||||
case TABLET = 'tablet';
|
||||
case UNKNOWN = 'unknown';
|
||||
|
||||
/**
|
||||
* Get human-readable display name
|
||||
*/
|
||||
public function getDisplayName(): string
|
||||
{
|
||||
return match ($this) {
|
||||
self::BOT => 'Bot',
|
||||
self::MOBILE => 'Mobile Device',
|
||||
self::DESKTOP => 'Desktop Computer',
|
||||
self::TABLET => 'Tablet',
|
||||
self::UNKNOWN => 'Unknown Device',
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if device is mobile (includes tablets)
|
||||
*/
|
||||
public function isMobile(): bool
|
||||
{
|
||||
return match ($this) {
|
||||
self::MOBILE, self::TABLET => true,
|
||||
default => false,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if device is desktop
|
||||
*/
|
||||
public function isDesktop(): bool
|
||||
{
|
||||
return $this === self::DESKTOP;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if device is a bot
|
||||
*/
|
||||
public function isBot(): bool
|
||||
{
|
||||
return $this === self::BOT;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user