Skip to content

Commit

Permalink
Merge branch 'ilaoniu-fix-cache' into 4.8.x
Browse files Browse the repository at this point in the history
  • Loading branch information
serbanghita committed Dec 8, 2024
2 parents 0260a98 + 807f5ee commit 29ac749
Show file tree
Hide file tree
Showing 4 changed files with 106 additions and 14 deletions.
3 changes: 3 additions & 0 deletions src/Cache/Cache.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@

use Psr\SimpleCache\CacheInterface;

/**
* Generic implementation of a cache system using an associative array.
*/
class Cache implements CacheInterface
{
/**
Expand Down
44 changes: 30 additions & 14 deletions src/MobileDetect.php
Original file line number Diff line number Diff line change
Expand Up @@ -250,7 +250,12 @@ class MobileDetect
'autoInitOfHttpHeaders' => true,
// Maximum HTTP User-Agent value allowed.
// @var int
'maximumUserAgentLength' => 500
'maximumUserAgentLength' => 500,
// Function that creates the cache key. e.g. (base64, sha1, custom fn).
'cacheKeyFn' => 'base64_encode',
// Cache TTL
// @var null|int|\DateInterval
'cacheTtl' => 86400,
];

/**
Expand Down Expand Up @@ -1403,7 +1408,7 @@ public function isMobile(): bool
try {
$cacheKey = $this->createCacheKey("mobile");
$cacheItem = $this->cache->get($cacheKey);
if (!is_null($cacheItem)) {
if ($cacheItem !== null) {
return $cacheItem->get();
}

Expand All @@ -1412,16 +1417,16 @@ public function isMobile(): bool
$this->getUserAgent() === self::$cloudFrontUA &&
$this->getHttpHeader('HTTP_CLOUDFRONT_IS_MOBILE_VIEWER') === 'true'
) {
$this->cache->set($cacheKey, true);
$this->cache->set($cacheKey, true, $this->config['cacheTtl']);
return true;
}

if ($this->hasHttpHeaders() && $this->checkHttpHeadersForMobile()) {
$this->cache->set($cacheKey, true);
$this->cache->set($cacheKey, true, $this->config['cacheTtl']);
return true;
} else {
$result = $this->matchUserAgentWithFirstFoundMatchingRule();
$this->cache->set($cacheKey, $result);
$this->cache->set($cacheKey, $result, $this->config['cacheTtl']);
return $result;
}
} catch (CacheException $e) {
Expand Down Expand Up @@ -1449,7 +1454,7 @@ public function isTablet(): bool
try {
$cacheKey = $this->createCacheKey("tablet");
$cacheItem = $this->cache->get($cacheKey);
if (!is_null($cacheItem)) {
if ($cacheItem !== null) {
return $cacheItem->get();
}

Expand All @@ -1458,7 +1463,7 @@ public function isTablet(): bool
$this->getUserAgent() === self::$cloudFrontUA &&
$this->getHttpHeader('HTTP_CLOUDFRONT_IS_TABLET_VIEWER') === 'true'
) {
$this->cache->set($cacheKey, true);
$this->cache->set($cacheKey, true, $this->config['cacheTtl']);
return true;
}

Expand All @@ -1469,28 +1474,28 @@ public function isTablet(): bool
$regexString = implode("|", $_regex);
}
if ($this->match($regexString, $this->getUserAgent())) {
$this->cache->set($cacheKey, true);
$this->cache->set($cacheKey, true, $this->config['cacheTtl']);
return true;
}

// if (is_array($_regex)) {
// foreach ($_regex as $regexString) {
// $result = $this->match($regexString, $this->getUserAgent());
// if ($result) {
// $this->cache->set($cacheKey, true);
// $this->cache->set($cacheKey, true, $this->config['cacheTtl']);
// return true;
// }
// }
// } else {
// // assume the regex is a "string"
// if ($this->match($_regex, $this->getUserAgent())) {
// $this->cache->set($cacheKey, true);
// $this->cache->set($cacheKey, true, $this->config['cacheTtl']);
// return true;
// }
// }
}

$this->cache->set($cacheKey, false);
$this->cache->set($cacheKey, false, $this->config['cacheTtl']);
return false;
} catch (CacheException $e) {
throw new MobileDetectException("Cache problem in isTablet(): {$e->getMessage()}");
Expand Down Expand Up @@ -1518,14 +1523,14 @@ public function is(string $ruleName): bool
try {
$cacheKey = $this->createCacheKey($ruleName);
$cacheItem = $this->cache->get($cacheKey);
if ($cacheItem) {
if ($cacheItem !== null) {
return $cacheItem->get();
}

$result = $this->matchUserAgentWithRule($ruleName);

// Cache save.
$this->cache->set($cacheKey, $result);
$this->cache->set($cacheKey, $result, $this->config['cacheTtl']);
return $result;
} catch (CacheException $e) {
throw new MobileDetectException("Cache problem in is(): {$e->getMessage()}");
Expand Down Expand Up @@ -1697,12 +1702,23 @@ public function getCache(): Cache
return $this->cache;
}

/**
* @throws CacheException
*/
protected function createCacheKey(string $key): string
{
$userAgentKey = $this->hasUserAgent() ? $this->userAgent : '';
$httpHeadersKey = $this->hasHttpHeaders() ? static::flattenHeaders($this->httpHeaders) : '';

return base64_encode("$key:$userAgentKey:$httpHeadersKey");
$cacheKey = "$key:$userAgentKey:$httpHeadersKey";

$cacheKeyFn = $this->config['cacheKeyFn'];

if (!is_callable($cacheKeyFn)) {
throw new CacheException('cacheKeyFn is not a function.');
}

return call_user_func($cacheKeyFn, $cacheKey);
}

public static function flattenHeaders(array $httpHeaders): string
Expand Down
52 changes: 52 additions & 0 deletions tests/MobileDetectWithCacheTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

namespace DetectionTests;

use Detection\Cache\Cache;
use Detection\Cache\CacheItem;
use Detection\Exception\MobileDetectException;
use Detection\MobileDetect;
use PHPUnit\Framework\TestCase;
Expand Down Expand Up @@ -112,4 +114,54 @@ public function testDefaultCacheClassCreatesMultipleCacheRecordsForAllCalls()
base64_decode($detect->getCache()->getKeys()[3])
);
}

/**
* @throws MobileDetectException
*/
public function testCustomCacheWithInvalidFnThrowsException()
{
$this->expectException(MobileDetectException::class);
$this->expectExceptionMessage('Cache problem in isMobile(): cacheKeyFn is not a function.');
$cache = new Cache();

$detect = new MobileDetect($cache, ['cacheKeyFn' => 'not a function']);
$detect->setUserAgent('iPad; AppleWebKit/533.17.9 Version/5.0.2 Mobile/8C148 Safari/6533.18.5');
$detect->isMobile();
}

public function testCustomCacheForConsecutiveCalls()
{
$cache = new Cache();

$detect = new MobileDetect($cache, ['cacheKeyFn' => fn ($key) => base64_encode($key)]);
$detect->setUserAgent('iPad; AppleWebKit/533.17.9 Version/5.0.2 Mobile/8C148 Safari/6533.18.5');

$detect->isMobile();
$this->assertCount(1, $cache->getKeys());

$detect->isMobile();
$this->assertCount(1, $cache->getKeys());
}

public function testGetCacheKeyIsUsedInConsecutiveCallsIfFoundIn()
{
$cache = $this->getMockBuilder(Cache::class)
->onlyMethods(["get", "set"])
->getMock();
$cache->method('get')->withAnyParameters()->willReturn(new CacheItem('name', 'value'));
$cache->method('set')->withAnyParameters()->willReturn(true);


$cache->expects($spy = $this->exactly(2))->method('get');
$cache->expects($spy = $this->never())->method('set');



$detect = new MobileDetect($cache);
$detect->setUserAgent('iPad; AppleWebKit/533.17.9 Version/5.0.2 Mobile/8C148 Safari/6533.18.5');

$detect->isMobile();
$detect->isMobile();

}
}
21 changes: 21 additions & 0 deletions tests/benchmark/MobileDetectBench.php
Original file line number Diff line number Diff line change
Expand Up @@ -124,4 +124,25 @@ public function benchIsSamsungTablet(): void
$detect->setUserAgent('Mozilla/5.0 (Linux; Android 12; SM-X906C Build/QP1A.190711.020; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/80.0.3987.119 Mobile Safari/537.36');
$detect->isSamsungTablet();
}

public function benchIsMobileCacheKeyFnBase64AgainstBestMatch(): void
{
$detect = new MobileDetect(null, ['cacheKeyFn' => 'base64_encode']);
$detect->setUserAgent('iPhone');
$detect->isMobile();
}

public function benchIsMobileCacheKeyFnSha1AgainstBestMatch(): void
{
$detect = new MobileDetect(null, ['cacheKeyFn' => 'sha1']);
$detect->setUserAgent('iPhone');
$detect->isMobile();
}

public function benchIsMobileCacheKeyFnCustomCryptFnAgainstBestMatch(): void
{
$detect = new MobileDetect(null, ['cacheKeyFn' => fn ($key) => crypt($key, 'bla')]);
$detect->setUserAgent('iPhone');
$detect->isMobile();
}
}

0 comments on commit 29ac749

Please sign in to comment.