├── src ├── Device.php ├── DeviceDetectorServiceProvider.php ├── DeviceDetector.php └── CacheRepository.php ├── LICENSE.md ├── config └── device-detector.php ├── composer.json ├── README.md └── UPGRADE.md /src/Device.php: -------------------------------------------------------------------------------- 1 | env('DEVICE_DETECTOR_CACHE_STORE'), 18 | 19 | /* 20 | |-------------------------------------------------------------------------- 21 | | Cache Prefix 22 | |-------------------------------------------------------------------------- 23 | | 24 | | This option controls the prefix that will be used for all cache keys 25 | | created by the device detector. This helps isolate device detector 26 | | cache entries from other application cache entries. 27 | | 28 | */ 29 | 30 | 'cache_prefix' => env('DEVICE_DETECTOR_CACHE_PREFIX', 'device-detector:'), 31 | 32 | ]; 33 | -------------------------------------------------------------------------------- /src/DeviceDetectorServiceProvider.php: -------------------------------------------------------------------------------- 1 | mergeConfigFrom(__DIR__.'/../config/device-detector.php', 'device-detector'); 18 | 19 | $this->app->singleton(DeviceDetector::class, DeviceDetector::class); 20 | 21 | $this->app->singleton(CacheRepository::class, function ($app) { 22 | return new CacheRepository( 23 | $app->cache->store($app->config->get('device-detector.cache_store')), 24 | $app->config->get('device-detector.cache_prefix', CacheRepository::DEFAULT_PREFIX) 25 | ); 26 | }); 27 | } 28 | 29 | /** 30 | * Bootstrap any application services. 31 | * 32 | * @return void 33 | */ 34 | public function boot(): void 35 | { 36 | if ($this->app->runningInConsole()) { 37 | $this->publishes([ 38 | __DIR__.'/../config/device-detector.php' => config_path('device-detector.php'), 39 | ], 'config'); 40 | } 41 | 42 | Request::macro('device', function () { 43 | /** @var \Illuminate\Http\Request $this */ 44 | return app(DeviceDetector::class)->detectRequest($this); 45 | }); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "reefki/laravel-device-detector", 3 | "description": "Laravel wrapper for Matomo's Universal Device Detection library.", 4 | "type": "library", 5 | "license": "MIT", 6 | "authors": [ 7 | { 8 | "name": "Rifki Aria Gumelar", 9 | "email": "rifki@rifki.net" 10 | } 11 | ], 12 | "require": { 13 | "php": "^8.1", 14 | "illuminate/cache": "^10.0|^11.0|^12.0", 15 | "illuminate/http": "^10.0|^11.0|^12.0", 16 | "illuminate/support": "^10.0|^11.0|^12.0", 17 | "matomo/device-detector": "^6.0" 18 | }, 19 | "require-dev": { 20 | "orchestra/testbench": "^8.0|^9.0|^10.0", 21 | "phpunit/phpunit": "^10.5|^11.0", 22 | "laravel/pint": "^1.13", 23 | "phpstan/phpstan": "^1.10" 24 | }, 25 | "autoload": { 26 | "psr-4": { 27 | "Reefki\\DeviceDetector\\": "src/" 28 | } 29 | }, 30 | "autoload-dev": { 31 | "psr-4": { 32 | "Reefki\\DeviceDetector\\Tests\\": "tests/" 33 | } 34 | }, 35 | "extra": { 36 | "laravel": { 37 | "providers": [ 38 | "Reefki\\DeviceDetector\\DeviceDetectorServiceProvider" 39 | ], 40 | "aliases": { 41 | "Device": "Reefki\\DeviceDetector\\Device" 42 | } 43 | } 44 | }, 45 | "config": { 46 | "sort-packages": true 47 | }, 48 | "minimum-stability": "stable", 49 | "prefer-stable": true, 50 | "scripts": { 51 | "test": "vendor/bin/phpunit", 52 | "ftest": "vendor/bin/phpunit --filter", 53 | "lint": "vendor/bin/pint", 54 | "analyse": "vendor/bin/phpstan analyse" 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/DeviceDetector.php: -------------------------------------------------------------------------------- 1 | cache = $cache; 27 | } 28 | 29 | /** 30 | * Detect the device using the user agent and headers. 31 | * 32 | * @param string $userAgent 33 | * @param array $headers 34 | * @return \DeviceDetector\DeviceDetector 35 | */ 36 | public function detect(string $userAgent, array $headers = []): MatomoDeviceDetector 37 | { 38 | $clientHints = ClientHints::factory( 39 | headers: $headers, 40 | ); 41 | 42 | $detector = new MatomoDeviceDetector( 43 | userAgent: $userAgent, 44 | clientHints: $clientHints, 45 | ); 46 | 47 | $detector->setCache( 48 | cache: $this->cache, 49 | ); 50 | 51 | $detector->parse(); 52 | 53 | return $detector; 54 | } 55 | 56 | /** 57 | * Detect the device for the given request. 58 | * 59 | * @param \Illuminate\Http\Request $request 60 | * @return \DeviceDetector\DeviceDetector 61 | */ 62 | public function detectRequest(Request $request): MatomoDeviceDetector 63 | { 64 | return $this->detect( 65 | userAgent: $request->userAgent() ?? '', 66 | headers: (array) $request->server(), 67 | ); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Laravel Device Detector 2 | === 3 | 4 | [![tests](https://github.com/reefki/laravel-device-detector/actions/workflows/tests.yml/badge.svg)](https://github.com/reefki/laravel-device-detector/actions/workflows/tests.yml) 5 | 6 | The Laravel wrapper for [Matomo Universal Device Detection](https://github.com/matomo-org/device-detector) library seamlessly integrates device detection capabilities into Laravel applications. 7 | 8 | ## Installation 9 | 10 | This package can be installed through Composer. 11 | 12 | ```bash 13 | composer require reefki/laravel-device-detector 14 | ``` 15 | 16 | Optionally, you can publish the config file of this package with this command: 17 | 18 | ```bash 19 | php artisan vendor:publish --provider="Reefki\DeviceDetector\DeviceDetectorServiceProvider" --tag="config" 20 | ``` 21 | 22 | ## Usage 23 | Detect device from a user agent string: 24 | 25 | ```php 26 | use Reefki\DeviceDetector\Device; 27 | 28 | $device = Device::detect($request->userAgent()); 29 | ``` 30 | 31 | Detect device from a user agent string and optional hints: 32 | 33 | ```php 34 | use Reefki\DeviceDetector\Device; 35 | 36 | $device = Device::detect($request->userAgent(), $request->server()); 37 | ``` 38 | 39 | Detect device from a request instance: 40 | 41 | ```php 42 | use Reefki\DeviceDetector\Device; 43 | 44 | $device = Device::detectRequest($request); 45 | ``` 46 | 47 | Detect device directly from a request instance: 48 | 49 | ```php 50 | $device = $request->device(); 51 | ``` 52 | 53 | All of the above methods wil return a `DeviceDetector\DeviceDetector` instance which you can use to get the information about the device: 54 | 55 | ```php 56 | if ($device->isBot()) { 57 | $botInfo = $device->getBot(); 58 | } else { 59 | $clientInfo = $device->getClient(); 60 | $osInfo = $device->getOs(); 61 | $deviceName = $device->getDeviceName(); 62 | $brand = $device->getBrandName(); 63 | $model = $device->getModel(); 64 | } 65 | ``` 66 | 67 | Read the [Matomo's Universal Device Detection](https://github.com/matomo-org/device-detector/blob/master/README.md) library readme for more information on how to use the returned instance. 68 | 69 | ## Testing 70 | 71 | Run the tests with: 72 | 73 | ``` bash 74 | vendor/bin/phpunit 75 | ``` 76 | 77 | ## Credits 78 | 79 | - [Matomo Universal Device Detection](https://github.com/matomo-org/device-detector) 80 | - [All Contributors](../../contributors) 81 | 82 | ## License 83 | 84 | The MIT License (MIT). Please see [License File](LICENSE.md) for more information. 85 | -------------------------------------------------------------------------------- /UPGRADE.md: -------------------------------------------------------------------------------- 1 | # Upgrade Guide 2 | 3 | ## Upgrading from 1.x to 2.0 4 | 5 | ### Requirements 6 | 7 | - **PHP**: Minimum version raised from 8.0 to 8.1 8 | - **Laravel**: Dropped support for Laravel 9, added support for Laravel 12 9 | 10 | If you're running PHP 8.0 or Laravel 9, you must upgrade before using v2.0. 11 | 12 | ### Cache Changes 13 | 14 | #### Prefixed Cache Keys 15 | 16 | Cache keys are now automatically prefixed with `device-detector:` to prevent collisions with other application cache entries. This means: 17 | 18 | - Existing cached device detection results from v1.x will **not** be found 19 | - They will simply be re-cached on first access (no errors, just a cache miss) 20 | - No action required unless you were accessing cache keys directly 21 | 22 | If you need a custom prefix, you can configure it in `config/device-detector.php`: 23 | 24 | ```php 25 | 'cache_prefix' => env('DEVICE_DETECTOR_CACHE_PREFIX', 'my-custom-prefix:'), 26 | ``` 27 | 28 | #### `flushAll()` Behavior 29 | 30 | The `CacheRepository::flushAll()` method now only removes device detector cache entries instead of flushing the entire cache store. 31 | 32 | **Before (v1.x):** 33 | ```php 34 | // This would flush ALL cache entries in the store 35 | $cacheRepository->flushAll(); 36 | ``` 37 | 38 | **After (v2.0):** 39 | ```php 40 | // This only flushes device detector entries 41 | $cacheRepository->flushAll(); 42 | ``` 43 | 44 | If you relied on the old behavior to flush the entire cache store, use Laravel's cache directly: 45 | 46 | ```php 47 | use Illuminate\Support\Facades\Cache; 48 | 49 | Cache::flush(); 50 | ``` 51 | 52 | ### CacheRepository Changes 53 | 54 | If you were extending or instantiating `CacheRepository` directly, note the constructor signature has changed: 55 | 56 | **Before (v1.x):** 57 | ```php 58 | public function __construct(Repository $cache) 59 | ``` 60 | 61 | **After (v2.0):** 62 | ```php 63 | public function __construct(Repository $cache, string $prefix = self::DEFAULT_PREFIX) 64 | ``` 65 | 66 | The second parameter is optional and defaults to `device-detector:`, so existing code instantiating `CacheRepository` with just the cache repository will continue to work. 67 | 68 | ### Configuration 69 | 70 | A new configuration option has been added. If you've published the config file, you may want to add: 71 | 72 | ```php 73 | // config/device-detector.php 74 | 75 | return [ 76 | 'cache_store' => env('DEVICE_DETECTOR_CACHE_STORE'), 77 | 78 | // New in v2.0 79 | 'cache_prefix' => env('DEVICE_DETECTOR_CACHE_PREFIX', 'device-detector:'), 80 | ]; 81 | ``` 82 | 83 | Or republish the config file: 84 | 85 | ```bash 86 | php artisan vendor:publish --provider="Reefki\DeviceDetector\DeviceDetectorServiceProvider" --tag="config" --force 87 | ``` 88 | -------------------------------------------------------------------------------- /src/CacheRepository.php: -------------------------------------------------------------------------------- 1 | 35 | */ 36 | protected array $keys = []; 37 | 38 | /** 39 | * Create a new cache repository instance. 40 | * 41 | * @param \Illuminate\Cache\Repository $cache 42 | * @param string $prefix 43 | * @return void 44 | */ 45 | public function __construct(Repository $cache, string $prefix = self::DEFAULT_PREFIX) 46 | { 47 | $this->cache = $cache; 48 | $this->prefix = $prefix; 49 | 50 | /** @var array $keys */ 51 | $keys = $this->cache->get($this->getKeysKey(), []); 52 | $this->keys = $keys; 53 | } 54 | 55 | /** 56 | * Get the prefixed cache key. 57 | * 58 | * @param string $id 59 | * @return string 60 | */ 61 | protected function key(string $id): string 62 | { 63 | return $this->prefix.$id; 64 | } 65 | 66 | /** 67 | * Get the key used to store the list of cached keys. 68 | * 69 | * @return string 70 | */ 71 | protected function getKeysKey(): string 72 | { 73 | return $this->prefix.'keys'; 74 | } 75 | 76 | /** 77 | * Get the cache key prefix. 78 | * 79 | * @return string 80 | */ 81 | public function getPrefix(): string 82 | { 83 | return $this->prefix; 84 | } 85 | 86 | /** 87 | * Track a cache key. 88 | * 89 | * @param string $id 90 | * @return void 91 | */ 92 | protected function trackKey(string $id): void 93 | { 94 | if (! in_array($id, $this->keys, true)) { 95 | $this->keys[] = $id; 96 | $this->cache->forever($this->getKeysKey(), $this->keys); 97 | } 98 | } 99 | 100 | /** 101 | * Untrack a cache key. 102 | * 103 | * @param string $id 104 | * @return void 105 | */ 106 | protected function untrackKey(string $id): void 107 | { 108 | $this->keys = array_values(array_filter($this->keys, fn ($key) => $key !== $id)); 109 | $this->cache->forever($this->getKeysKey(), $this->keys); 110 | } 111 | 112 | /** 113 | * Retrieve an item from the cache by id. 114 | * 115 | * @param string $id 116 | * @return mixed 117 | */ 118 | public function fetch(string $id): mixed 119 | { 120 | return $this->cache->get($this->key($id)); 121 | } 122 | 123 | /** 124 | * Determine if an item exists in the cache. 125 | * 126 | * @param string $id 127 | * @return bool 128 | */ 129 | public function contains(string $id): bool 130 | { 131 | return $this->cache->has($this->key($id)); 132 | } 133 | 134 | /** 135 | * Store an item in the cache. 136 | * 137 | * @param string $id 138 | * @param mixed $data 139 | * @param int $lifeTime 140 | * @return bool 141 | */ 142 | public function save(string $id, mixed $data, int $lifeTime = 3600): bool 143 | { 144 | $this->trackKey($id); 145 | 146 | return $this->cache->put($this->key($id), $data, $lifeTime); 147 | } 148 | 149 | /** 150 | * Remove an item from the cache. 151 | * 152 | * @param string $id 153 | * @return bool 154 | */ 155 | public function delete(string $id): bool 156 | { 157 | $this->untrackKey($id); 158 | 159 | return $this->cache->forget($this->key($id)); 160 | } 161 | 162 | /** 163 | * Remove all device detector items from the cache. 164 | * 165 | * @return bool 166 | */ 167 | public function flushAll(): bool 168 | { 169 | foreach ($this->keys as $id) { 170 | $this->cache->forget($this->key($id)); 171 | } 172 | 173 | $this->keys = []; 174 | $this->cache->forget($this->getKeysKey()); 175 | 176 | return true; 177 | } 178 | } 179 | --------------------------------------------------------------------------------