Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 10 additions & 22 deletions .github/workflows/latest.yml
Original file line number Diff line number Diff line change
@@ -1,42 +1,30 @@
name: "Test on Latest Laravel"

# Triggers the workflow on push or pull request events
on: [push, pull_request]
on: [push, pull_request, workflow_dispatch]

jobs:
test:
name: PHPUnit Tests
build:
name: PHPUnit Hand Test
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v7

- name: Setup PHP
uses: shivammathur/setup-php@master
uses: shivammathur/setup-php@v2
with:
php-version: 8.2
php-version: 8.3

- name: Composer self update
- name: Composer update
run: composer self-update >/dev/null 2>&1

- name: Get composer cache directory
id: composer-cache
run: echo "::set-output name=dir::$(composer config cache-files-dir)"

- name: Cache dependencies
uses: actions/cache@v2
with:
path: ${{ steps.composer-cache.outputs.dir }}
key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.json') }}
restore-keys: ${{ runner.os }}-composer-

- name: Lock laravel/framework version
env:
LARAVEL_VERSION: 10.5.0
run: composer require laravel/framework:10.5.0 --no-update
LARAVEL_VERSION: 13.35.0
run: composer require laravel/framework:13.35.0 --no-update -W

- name: Vendor update
if: steps.composer-cache.outputs.cache-hit != 'true'
run: composer update --prefer-source --no-interaction

- name: Run test suites
Expand All @@ -45,4 +33,4 @@ jobs:
- name: Coveralls
env:
COVERALLS_REPO_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: php vendor/bin/php-coveralls -v
run: php vendor/bin/php-coveralls -v
52 changes: 0 additions & 52 deletions .github/workflows/test-php8.2.yml

This file was deleted.

1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,4 @@ demo/cache
.coveralls.yml export-ignore
phpstan.neon export-ignore
phpunit.xml export-ignore
composer.lock
121 changes: 121 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
# Project Overview

Browser Detect (`hisorange/browser-detect`) is a PHP library that identifies a visitor's browser, operating system, and device type (mobile / tablet / desktop / bot) by piping the HTTP user-agent string through five well-known detection engines instead of relying on custom heuristics. It targets Laravel 9.x–10.x apps (with a standalone, Laravel-free mode for any PHP 8.1+ project) and exposes a single `Browser` facade plus Blade directives (`@mobile`, `@tablet`, `@desktop`, `@browser`), caching every parse so repeated lookups cost under 0.02 ms.

## Repository Structure

- `src/` — all library code, PSR-4 namespace `hisorange\BrowserDetect\`.
- `src/Facade.php` — Laravel facade exposing the `Browser` alias API.
- `src/ServiceProvider.php` — binds the `browser-detect` service and registers Blade directives.
- `src/Parser.php` — orchestrator: reads the agent, enforces security limits, caches, and runs the pipeline.
- `src/Payload.php` — mutable state carrier passed down the pipeline.
- `src/Result.php` — immutable, typed result object with the public getters.
- `src/Contracts/` — interfaces (`ParserInterface`, `PayloadInterface`, `ResultInterface`, `StageInterface`) defining the pipeline contract.
- `src/Exceptions/` — namespaced exception hierarchy rooted at `Exception`.
- `src/Stages/` — one class per detection engine: `UAParser`, `MobileDetect`, `CrawlerDetect`, `DeviceDetector`, `BrowserDetect` (final reconciliation).
- `config/browser-detect.php` — default configuration (cache interval/prefix, max header length), publishable via `php artisan vendor:publish`.
- `tests/` — PHPUnit + Orchestra/Testbench suite, one `*Test.php` per class; `tests/Stages/` mirrors stage tests; `tests/logs/` holds generated coverage output.
- `vendor/` — Composer dependencies (ua-parser, mobiledetect, crawler-detect, device-detector, league/pipeline, testbench); never hand-edit.
- `.github/workflows/` — CI: `latest.yml` (push/PR) and `test-php8.2.yml` (manual version matrix).
- Root files — `composer.json`, `phpunit.xml`, `phpstan.neon`, `.coveralls.yml`, `README.md`, `CHANGELOG.md`, `LICENSE`.

## Build & Development Commands

Install (consumer side):

```sh
composer require hisorange/browser-detect
```

In a Laravel app, publish the config file:

```sh
php artisan vendor:publish
```

Run the test suite (commands preserved verbatim from `composer.json` scripts):

```sh
composer run-script test-dev # phpunit, no coverage
composer run-script test # phpunit --coverage-clover ./tests/logs/clover.xml
```

Type-check (phpstan level max, config in `phpstan.neon`; command as used in CI):

```sh
vendor/bin/phpstan analyse -c phpstan.neon ./src/
```

Lint (PSR-12; currently commented out in CI, no root phpcs.xml):

```sh
php vendor/bin/phpcs --standard=PSR12 ./src/
```

## Code Style & Conventions

- PHP files follow PSR-12 (enforced via the CI phpcs line); 4-space indentation, one class per file.
- Namespaces mirror directories: `hisorange\BrowserDetect\Stages\UAParser` lives at `src/Stages/UAParser.php` (PSR-4 mapping in `composer.json`).
- Classes are PascalCase nouns; methods are camelCase; boolean getters use the `is*` prefix (`isMobile`, `isIEVersion`).
- Every public method carries a docblock with `@param`/`@return` (or `@inheritdoc`); return types are declared — recent commits added missing ones.
- Interfaces live in `Contracts/` and are implemented, never duck-typed; exceptions are the namespaced ones in `Exceptions/`, never built-ins directly.
- Commit template (observed in history): `type: summary (#issue)` — e.g. `chore: adding some missed return types (#209)`; plain `Fix ... (#204)` also occurs. Prefer conventional prefixes (`docs:`, `chore:`, `fix:`) with the upstream issue number.

## Architecture Notes

```
user-agent string
| (truncated to config.security.max-header-length = 2048 bytes)
v
Parser.detect() --> Parser.parse(agent)
| key = "bd4_" + md5(agent)
+-- runtime[] (in-memory, per page load)
+-- Laravel CacheManager.remember(key, interval=10080s)
v
League\Pipeline (src/Parser.php:204-211)
UAParser -> MobileDetect -> CrawlerDetect -> DeviceDetector -> BrowserDetect
| each Stage.__invoke(Payload) mutates Payload key/value store
v
new Result(payload.toArray()) --> Facade / Blade reflect calls onto Result
```

Prose: `ServiceProvider.register()` binds the DI key `browser-detect` to a `Parser` wired with the app's `cache`, `request`, and merged config. `Parser` is the only stateful component: it truncates the agent (DoS guard), hashes it, and consults a two-tier cache (runtime array + Laravel cache). On a miss, `process()` pipes a fresh `Payload` through the five stages in fixed order — UAParser seeds browser/OS/device fields, MobileDetect and CrawlerDetect add mobile/bot flags, DeviceDetector refines platform and engine details (skipped for bots), and `BrowserDetect` reconciles conflicting flags (exactly one of mobile/tablet/desktop, Prerender-as-bot, vendor booleans, human-readable version strings, in-app detection) before sealing everything into an immutable `Result`. `Facade.__call` and `Parser.__call` reflect any API call onto the `ResultInterface`, so the public API surface is exactly the Result's getters.

## Testing Strategy

- Framework: PHPUnit 9/10 with `orchestra/testbench` (Laravel app harness); bootstrap is `./vendor/autoload.php` per `phpunit.xml`.
- Suite layout: one `*Test.php` per source class under `tests/` and `tests/Stages/`, all extending `tests/TestCase.php` which registers the provider and the `Browser` alias; `phpunit.xml` discovers files with suffix `Test.php` and excludes `./tests/_fixture`.
- Local: `composer run-script test-dev` (fast) or `composer run-script test` (writes clover to `tests/logs/clover.xml`).
- CI: `.github/workflows/latest.yml` runs on every push/PR with PHP 8.2 + Laravel 10.5.0, then uploads coverage via `php vendor/bin/php-coveralls -v` using `COVERALLS_REPO_TOKEN` (`.coveralls.yml` points at `tests/logs/clover.xml`).
- Version matrix: `test-php8.2.yml` (manual `workflow_dispatch`) tests Laravel 9.52–10.4; the README matrix documents which library majors support which Laravel/PHP combinations — keep changes compatible with the supported matrix.

## Security & Compliance

- DoS guard: agent strings are cut at `config.security.max-header-length` (2048 bytes) before any regex engine sees them — never remove this truncation.
- No secrets in the repo; CI uses `GITHUB_TOKEN` only. Never commit tokens; `.gitignore` already excludes caches and editor dirs.
- Dependency scanning: run `composer list --outdated` / audit via your vendor service; > TODO: repo has no dedicated dependency-scanning config.
- License: MIT (`LICENSE`, © Varga Zsolt); bundled deps carry their own licenses inside `vendor/` — do not relicense or edit them.
- Cache keys are `prefix + md5(agent)`; keep the `bd4_` prefix so old and new cache entries never collide.

## Agent Guardrails

- Never modify: `vendor/`, `composer.lock`, `tests/logs/`, `.phpunit.cache/`, generated coverage output.
- Do not touch `src/Contracts/` signatures or `config/browser-detect.php` defaults without an explicit request — they are public API and published config.
- Any change to `src/Stages/*` or `src/Parser.php` must keep all `*Test.php` suites green before being proposed.
- Preserve the pipeline stage order in `Parser.process()`; reordering changes precedence semantics (later stages overwrite earlier values).
- > TODO: repo has no CODEOWNERS or branch protection config; require maintainer review for releases per CHANGELOG flow.

## Extensibility Hooks

- Config override: pass a third ctor arg to `Parser` (`cache.interval`, `cache.prefix`, `security.max-header-length`); Laravel merges `config/browser-detect.php` via `mergeConfigFrom`.
- Custom stages: any class implementing `StageInterface` with `__invoke(Payload): Payload` can be inserted into the `league/pipeline` chain in `Parser.process()`.
- DI hooks: service key `browser-detect`, facade accessor `'browser-detect'`, Blade directives `@mobile/@tablet/@desktop/@browser(fn)`.
- Standalone mode: `hisorange\BrowserDetect\Parser` static singleton reads `$_SERVER['HTTP_USER_AGENT']` and works without a cache.
- > TODO: no env vars or feature flags exist in the codebase; configuration is file/array based only.

## Further Reading

- [README.md](README.md) — full API table, install, Blade usage, version matrix.
- [CHANGELOG.md](CHANGELOG.md) — release history and supported-version changes.
- [phpstan.neon](phpstan.neon) and [phpunit.xml](phpunit.xml) — analyzer and test configuration.
- [.github/workflows/latest.yml](.github/workflows/latest.yml) — canonical CI/test procedure.
11 changes: 11 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,14 @@
### Changes in 5.1.0

---

- Support for Laravel 11.x
- Support for Laravel 12.x
- Support for Laravel 13.x
- Test on PHP 8.3 too
- Upgraded the test harness to orchestra/testbench 10.x
- General modernization to use the newest dependencies

### Changes in 5.0.0

---
Expand Down
10 changes: 6 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,18 +1,17 @@
![Browser Detection Logo](https://user-images.githubusercontent.com/3441017/126362397-d9767164-4f44-4d41-a3cd-b669e10e95dc.png)

## Browser Detection v5.0 by _[hisorange](https://hisorange.me)_
## Browser Detection v5.1 by _[hisorange](https://www.linkedin.com/in/varga-zsolt/)_

[![Latest Stable Version](https://poser.pugx.org/hisorange/browser-detect/v/stable)](https://packagist.org/packages/hisorange/browser-detect)
[![Build](https://github.com/hisorange/browser-detect/actions/workflows/latest.yml/badge.svg?branch=stable)](https://github.com/hisorange/browser-detect/actions/workflows/latest.yml)
[![Coverage Status](https://coveralls.io/repos/github/hisorange/browser-detect/badge.svg)](https://coveralls.io/github/hisorange/browser-detect)
[![Total Downloads](https://poser.pugx.org/hisorange/browser-detect/downloads)](https://packagist.org/packages/hisorange/browser-detect)
[![License](https://poser.pugx.org/hisorange/browser-detect/license)](https://packagist.org/packages/hisorange/browser-detect)

Easy to use package to identify the visitor's browser details and device type.
Magic is **not** involved the results are generated by multiple well tested and developed packages.

Supports **every Laravel** version between **4.0 » 10.x**;
Also tested on **every PHP** version between **5.6 » 8.2**.
Supports **every Laravel** version between **4.0 » 13.x**;
Also tested on **every PHP** version between **5.6 » 8.5**.

### How to install

Expand Down Expand Up @@ -100,6 +99,9 @@ The following matrix has been continuously tested by the great and awesome **Git
| Laravel 8.x | - | - | - | 4.4+ | - |
| Laravel 9.x | - | - | - | 4.4+ | ✓ |
| Laravel 10.x | - | - | - | - | ✓ |
| Laravel 11.x | - | - | - | - | ✓ |
| Laravel 12.x | - | - | - | - | ✓ |
| Laravel 13.x | - | - | - | - | ✓ |
| Standalone | - | - | - | 4.2+ | ✓ |

Since 2013 the package runs tests on every possible PHP / Laravel version matrix.
Expand Down
17 changes: 12 additions & 5 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,21 +16,21 @@
"license": "MIT",
"authors": [
{
"name": "Varga Zsolt",
"email": "hello@hisorange.me"
"name": "Zsolt Varga",
"email": "hisorange@proton.me"
}
],
"require": {
"php": "^8.1",
"php": "^8.2",
"ua-parser/uap-php": "~3.9",
"league/pipeline": "^1.0",
"mobiledetect/mobiledetectlib": "~4.0",
"jaybizzle/crawler-detect": "~1.2",
"matomo/device-detector": "^6.0"
},
"require-dev": {
"phpunit/phpunit": "~9.0 || ~10.0",
"orchestra/testbench": "~7.0 || ~8.0",
"phpunit/phpunit": "~11.0",
"orchestra/testbench": "~10.0 || ~11.0",
"php-coveralls/php-coveralls": "~2.0"
},
"autoload": {
Expand All @@ -57,5 +57,12 @@
"scripts": {
"test-dev": "phpunit",
"test": "phpunit --coverage-clover ./tests/logs/clover.xml"
},
"config": {
"policy": {
"advisories": {
"block": false
}
}
}
}
Loading
Loading