From e20d7ed577b6f28ba4fc2a12a157bae464222fca Mon Sep 17 00:00:00 2001 From: Brian Higby Date: Mon, 4 May 2026 21:15:59 -0400 Subject: [PATCH 1/3] feat(cache): add device-detector cache option to improve performance --- README.md | 11 ++++++----- config/browser-detect.php | 7 +++++++ src/Parser.php | 6 +++--- src/Stages/DeviceDetector.php | 5 +++++ tests/Stages/DeviceDetectorTest.php | 19 +++++++++++++++++++ 5 files changed, 40 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 6048a79..bf6b4b0 100644 --- a/README.md +++ b/README.md @@ -166,11 +166,12 @@ $browser = new Parser(null, null, [ Available options: -| Key | Default | Description | -| :--------------------------- | :-----: | :------------------------------------- | -| `cache.interval` | `10080` | Cache TTL in seconds | -| `cache.prefix` | `bd4_` | Cache key prefix | -| `security.max-header-length` | `2048` | Max user agent length (DoS protection) | +| Key | Default | Description | +| :--------------------------- | :-------: | :----------------------------------------------------- | +| `cache.interval` | `10080` | Cache TTL in seconds | +| `cache.prefix` | `bd4_` | Cache key prefix | +| `cache.device-detector` | `false` | Enable matomo/device-detector's internal Laravel cache | +| `security.max-header-length` | `2048` | Max user agent length (DoS protection) | ## Quality diff --git a/config/browser-detect.php b/config/browser-detect.php index 1c85b19..52ea01c 100644 --- a/config/browser-detect.php +++ b/config/browser-detect.php @@ -10,6 +10,13 @@ * Cache prefix, the user agent string will be hashed and appended at the end. */ 'prefix' => 'bd4_', + /** + * Enable the device-detector engine's own internal cache via Laravel's cache store. + * When enabled, parsed YAML device definition data is cached by the underlying + * matomo/device-detector library, reducing file reads on repeated parses. + * Requires a Laravel application context — do not enable in standalone mode. + */ + 'device-detector' => false, ], 'security' => [ /** diff --git a/src/Parser.php b/src/Parser.php index b815f57..eff2819 100644 --- a/src/Parser.php +++ b/src/Parser.php @@ -81,7 +81,7 @@ public function __construct(?CacheManager $cache = null, ?Request $request = nul $this->pipeline = [ new Stages\CrawlerDetect, - new Stages\DeviceDetector, + new Stages\DeviceDetector($this->cacheConfig()['device-detector']), new Stages\BrowserDetect, ]; } @@ -202,11 +202,11 @@ protected function makeHashKey(string $agent): string } /** - * @return array{interval: int, prefix: string} + * @return array{interval: int, prefix: string, device-detector: bool} */ private function cacheConfig(): array { - /** @var array{interval: int, prefix: string} */ + /** @var array{interval: int, prefix: string, device-detector: bool} */ return $this->config['cache']; } diff --git a/src/Stages/DeviceDetector.php b/src/Stages/DeviceDetector.php index db57ee6..cc681fc 100644 --- a/src/Stages/DeviceDetector.php +++ b/src/Stages/DeviceDetector.php @@ -13,12 +13,17 @@ class DeviceDetector implements StageInterface { protected ?\DeviceDetector\DeviceDetector $detector = null; + public function __construct(protected bool $useDeviceDetectorCache = false) {} + public function __invoke(PayloadInterface $payload): PayloadInterface { if ($this->detector === null) { $this->detector = new \DeviceDetector\DeviceDetector; // Skip bot detection — CrawlerDetect handles that upstream. $this->detector->skipBotDetection(true); + if ($this->useDeviceDetectorCache) { + $this->detector->setCache(new \DeviceDetector\Cache\LaravelCache()); + } } $this->detector->setUserAgent($payload->getAgent()); $this->detector->parse(); diff --git a/tests/Stages/DeviceDetectorTest.php b/tests/Stages/DeviceDetectorTest.php index 9244ba5..61fe1b8 100644 --- a/tests/Stages/DeviceDetectorTest.php +++ b/tests/Stages/DeviceDetectorTest.php @@ -5,6 +5,7 @@ use hisorange\BrowserDetect\Payload; use hisorange\BrowserDetect\Stages\DeviceDetector; use hisorange\BrowserDetect\Test\TestCase; +use Illuminate\Support\Facades\Cache; use PHPUnit\Framework\Attributes\DataProvider; /** @@ -77,6 +78,24 @@ public static function provideAgents() ]; } + /** + * @covers ::__construct() + * @covers ::__invoke() + */ + public function test_invoke_with_device_detector_cache_enabled() + { + // Must be called before the stage runs to intercept cache writes. + Cache::spy(); + + $stage = new DeviceDetector(useDeviceDetectorCache: true); + $payload = new Payload('Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.108 Safari/537.36'); + $stage($payload); + + $this->assertSame('Blink', $payload->getValue('browserEngine')); + $this->assertSame('Chrome', $payload->getValue('browserFamily')); + Cache::shouldHaveReceived('put'); + } + /** * @covers ::__invoke() */ From 6780ec5bc866228da1322fa9dade8abd21d73a40 Mon Sep 17 00:00:00 2001 From: Brian Higby Date: Tue, 5 May 2026 05:18:54 -0400 Subject: [PATCH 2/3] feat(cache): update device-detector cache configuration options --- README.md | 2 +- config/browser-detect.php | 12 +++++++----- src/Parser.php | 5 +++-- src/Stages/DeviceDetector.php | 11 ++++++++--- tests/Stages/DeviceDetectorTest.php | 3 ++- 5 files changed, 21 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index bf6b4b0..7111187 100644 --- a/README.md +++ b/README.md @@ -170,7 +170,7 @@ Available options: | :--------------------------- | :-------: | :----------------------------------------------------- | | `cache.interval` | `10080` | Cache TTL in seconds | | `cache.prefix` | `bd4_` | Cache key prefix | -| `cache.device-detector` | `false` | Enable matomo/device-detector's internal Laravel cache | +| `cache.device-detector` | `null` | Cache driver for device-detector's internal cache. See `config/browser-detect.php` for examples. | | `security.max-header-length` | `2048` | Max user agent length (DoS protection) | ## Quality diff --git a/config/browser-detect.php b/config/browser-detect.php index 52ea01c..68af1c4 100644 --- a/config/browser-detect.php +++ b/config/browser-detect.php @@ -11,12 +11,14 @@ */ 'prefix' => 'bd4_', /** - * Enable the device-detector engine's own internal cache via Laravel's cache store. - * When enabled, parsed YAML device definition data is cached by the underlying - * matomo/device-detector library, reducing file reads on repeated parses. - * Requires a Laravel application context — do not enable in standalone mode. + * Cache driver class for the device-detector engine's internal cache. When null, + * the library uses its built-in StaticCache. Override to swap in a different driver. + * + * Examples: + * \DeviceDetector\Cache\StaticCache::class — in-process static cache (default) + * \DeviceDetector\Cache\LaravelCache::class — persists via Laravel's cache store */ - 'device-detector' => false, + 'device-detector' => null, ], 'security' => [ /** diff --git a/src/Parser.php b/src/Parser.php index eff2819..2b2b308 100644 --- a/src/Parser.php +++ b/src/Parser.php @@ -2,6 +2,7 @@ namespace hisorange\BrowserDetect; +use DeviceDetector\Cache\CacheInterface; use hisorange\BrowserDetect\Contracts\ParserInterface; use hisorange\BrowserDetect\Contracts\ResultInterface; use hisorange\BrowserDetect\Contracts\StageInterface; @@ -202,11 +203,11 @@ protected function makeHashKey(string $agent): string } /** - * @return array{interval: int, prefix: string, device-detector: bool} + * @return array{interval: int, prefix: string, device-detector: class-string|null} */ private function cacheConfig(): array { - /** @var array{interval: int, prefix: string, device-detector: bool} */ + /** @var array{interval: int, prefix: string, device-detector: class-string|null} */ return $this->config['cache']; } diff --git a/src/Stages/DeviceDetector.php b/src/Stages/DeviceDetector.php index cc681fc..038cea1 100644 --- a/src/Stages/DeviceDetector.php +++ b/src/Stages/DeviceDetector.php @@ -2,6 +2,7 @@ namespace hisorange\BrowserDetect\Stages; +use DeviceDetector\Cache\CacheInterface; use DeviceDetector\Parser\Device\AbstractDeviceParser; use hisorange\BrowserDetect\Contracts\PayloadInterface; use hisorange\BrowserDetect\Contracts\StageInterface; @@ -13,7 +14,10 @@ class DeviceDetector implements StageInterface { protected ?\DeviceDetector\DeviceDetector $detector = null; - public function __construct(protected bool $useDeviceDetectorCache = false) {} + /** + * @param class-string|null $deviceDetectorCache + */ + public function __construct(protected ?string $deviceDetectorCache = null) {} public function __invoke(PayloadInterface $payload): PayloadInterface { @@ -21,8 +25,9 @@ public function __invoke(PayloadInterface $payload): PayloadInterface $this->detector = new \DeviceDetector\DeviceDetector; // Skip bot detection — CrawlerDetect handles that upstream. $this->detector->skipBotDetection(true); - if ($this->useDeviceDetectorCache) { - $this->detector->setCache(new \DeviceDetector\Cache\LaravelCache()); + if ($this->deviceDetectorCache !== null) { + $cacheClass = $this->deviceDetectorCache; + $this->detector->setCache(new $cacheClass); } } $this->detector->setUserAgent($payload->getAgent()); diff --git a/tests/Stages/DeviceDetectorTest.php b/tests/Stages/DeviceDetectorTest.php index 61fe1b8..e1d2b2e 100644 --- a/tests/Stages/DeviceDetectorTest.php +++ b/tests/Stages/DeviceDetectorTest.php @@ -2,6 +2,7 @@ namespace hisorange\BrowserDetect\Test\Stages; +use DeviceDetector\Cache\LaravelCache; use hisorange\BrowserDetect\Payload; use hisorange\BrowserDetect\Stages\DeviceDetector; use hisorange\BrowserDetect\Test\TestCase; @@ -87,7 +88,7 @@ public function test_invoke_with_device_detector_cache_enabled() // Must be called before the stage runs to intercept cache writes. Cache::spy(); - $stage = new DeviceDetector(useDeviceDetectorCache: true); + $stage = new DeviceDetector(deviceDetectorCache: LaravelCache::class); $payload = new Payload('Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.108 Safari/537.36'); $stage($payload); From ad43172e539a843073902cc3a3d13b802f0a44b4 Mon Sep 17 00:00:00 2001 From: Brian Higby Date: Tue, 5 May 2026 19:47:01 -0400 Subject: [PATCH 3/3] test: fix cache spy usage in DeviceDetectorTest --- tests/Stages/DeviceDetectorTest.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/Stages/DeviceDetectorTest.php b/tests/Stages/DeviceDetectorTest.php index e1d2b2e..ea4e2a6 100644 --- a/tests/Stages/DeviceDetectorTest.php +++ b/tests/Stages/DeviceDetectorTest.php @@ -85,8 +85,8 @@ public static function provideAgents() */ public function test_invoke_with_device_detector_cache_enabled() { - // Must be called before the stage runs to intercept cache writes. - Cache::spy(); + $spy = \Mockery::spy(Cache::getFacadeRoot())->makePartial(); + Cache::swap($spy); $stage = new DeviceDetector(deviceDetectorCache: LaravelCache::class); $payload = new Payload('Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.108 Safari/537.36'); @@ -94,7 +94,7 @@ public function test_invoke_with_device_detector_cache_enabled() $this->assertSame('Blink', $payload->getValue('browserEngine')); $this->assertSame('Chrome', $payload->getValue('browserFamily')); - Cache::shouldHaveReceived('put'); + $spy->shouldHaveReceived('put'); } /**