A User-Agent is not an identity.
Photon emits the UA and the Sec-CH-UA Client Hints that must agree with it — matched brand, version, platform and mobile bit, in real send order.
User-Agent · Sec-CH-UA / platform / mobile · Accept headers · per-engine coherence · zero dependencies
⋆ ˚ 。 ⋆ ୨ ⋆ ˚ 。 ⋆
Why Photon · Install · Quick start · Coherence · Filters · With PulsarX · API
| Feature | What it does | |
|---|---|---|
| ✦ | Whole identity | UA + Sec-CH-UA + -Mobile + -Platform + Accept / Accept-Language / Accept-Encoding, generated as one unit. |
| ◈ | Engine-aware | Client Hints only for Chromium (Chrome / Edge). Firefox & Safari get none — because real ones send none. |
| ⟡ | Coherent | The version in Sec-CH-UA matches the UA. The mobile bit matches the platform. Safari only appears on macOS / iOS. |
| ⌗ | Real send order | headers() returns the set in the order that browser actually transmits — hints first for Chromium, UA first for the rest. |
| ✸ | GREASE | Rotates the Not?A_Brand GREASE brand and its position, the way Chromium does. |
| ⬡ | Zero deps | One class per file, a tiny autoloader, optional Composer. Pure PHP. |
Every "random user agent" package for PHP hands you a lone UA string. But since
Chrome 89, a Chromium request also carries User-Agent Client Hints —
Sec-CH-UA, Sec-CH-UA-Mobile, Sec-CH-UA-Platform — and anti-bot systems
cross-check them against the UA. A Chrome/131 UA next to a Sec-CH-UA that
says v="120", or a desktop UA with Sec-CH-UA-Mobile: ?1, is a louder
bot signal than no hints at all.
| Photon | campo/random-user-agent | trgino/random-user-agent | Faker | |
|---|---|---|---|---|
| UA string | ✓ | ✓ | ✓ | ✓ |
Sec-CH-UA hints |
✓ | ✗ | ✗ | ✗ |
| UA ↔ hints coherence | ✓ | — | — | — |
| Engine-correct (no hints for FF/Safari) | ✓ | — | — | — |
Accept / Accept-Language set |
✓ | ✗ | ✗ | ✗ |
| Real header send order | ✓ | ✗ | ✗ | ✗ |
| Runtime dependencies | none | none | none | several |
With Composer
composer require vxsilisk/photonWithout Composer — require the bundled autoloader:
require __DIR__ . '/autoload.php';Requirements: PHP ≥ 8.1. No extensions beyond core.
require __DIR__ . '/autoload.php';
$id = Photon::chrome('windows');
$id->userAgent(); // Mozilla/5.0 (Windows NT 10.0; Win64; x64) …Chrome/140.0.0.0 Safari/537.36
$id->secChUa(); // "Not?A_Brand";v="8", "Chromium";v="140", "Google Chrome";v="140"
$id->secChUaMobile(); // ?0
$id->secChUaPlatform(); // "Windows"
$id->headerLines(); // ['sec-ch-ua: …', 'sec-ch-ua-mobile: ?0', …, 'User-Agent: …']Or just take pot luck:
Photon::random(); // any coherent identity
Photon::mobile(); // any mobile identity
Photon::userAgent(); // just the stringThe whole value of Photon is that these can never contradict each other:
$id = Photon::random();
// 1. Client Hints exist iff the engine is Chromium.
$id->secChUa() === null ⟺ $id->engine() !== 'blink';
// 2. The brand version equals the UA's Chrome version.
str_contains($id->secChUa(), '"' . $id->version() . '"'); // always true for Chromium
// 3. The mobile bit agrees with the platform.
$id->isMobile() ⟺ $id->secChUaMobile() === '?1';
// 4. Impossible combinations throw, they don't silently lie.
Photon::safari('windows'); // PhotonException — Safari isn't on WindowsFirefox and Safari deliberately return null for every Sec-CH-UA* accessor,
and their header sets omit those lines entirely — matching what the real
browsers put on the wire.
Photon::chrome(); // Chrome, any OS it ships on
Photon::chrome('macos'); // Chrome on macOS
Photon::edge('windows');
Photon::firefox('linux');
Photon::safari('ios');
Photon::random(browser: 'chrome', os: 'android');
Photon::desktop(); // any desktop identity
Photon::mobile('chrome'); // mobile Chrome specifically| Browser | Engine | Platforms |
|---|---|---|
chrome |
blink | windows · macos · linux · android |
edge |
blink | windows · macos |
firefox |
gecko | windows · macos · linux · android |
safari |
webkit | macos · ios |
Reproducible output for tests and fixtures:
Photon::seed(42);
Photon::random(); // deterministic from herePhoton is the identity layer of the PulsarX
story. Where impersonate() pins one built-in profile, Photon mints a fresh,
coherent header set per request — drop it straight into any client:
$s = new Pulsar();
foreach ($urls as $url) {
$s->get($url, headers: Photon::chrome()->headerLines());
}Works just as well with cURL, Guzzle, or file_get_contents stream contexts —
headerLines() is the universal Name: value shape.
Generator — Photon (all static)
| Method | Returns |
|---|---|
random(?browser, ?os, ?device) |
Identity |
chrome(?os) / edge(?os) / firefox(?os) / safari(?os) |
Identity |
desktop(?browser) / mobile(?browser) |
Identity |
userAgent(?browser, ?os) |
string |
seed(int) |
void — reproducible RNG |
Identity (immutable)
| Method | Returns |
|---|---|
userAgent() |
string |
browser() / engine() / version() / platform() |
string |
isMobile() |
bool |
secChUa() / secChUaMobile() / secChUaPlatform() |
?string |
secChUaPlatformVersion() |
?string (high-entropy hint) |
accept() / acceptLanguage() / acceptEncoding() |
string |
headers() |
array<string,string> in send order |
headerLines() |
string[] — Name: value lines |
toArray() |
array |
Photon/
├── autoload.php # zero-dependency autoloader
├── composer.json
├── example.php # runnable demo
├── tests/run.php # framework-free test suite (incl. 500-identity coherence fuzz)
└── src/
├── Photon.php # the generator + coherence resolver
├── Identity.php # one immutable identity (headers, ordering)
├── Catalog.php # browser / OS / engine reference data
└── PhotonException.php
Run
php example.phpfor a live demo, orphp tests/run.phpfor the test suite.
⋆ ˚ 。 ⋆ ୨ ⋆ ˚ 。 ⋆
Photon — made by Vxsilisk · MIT License
Part of the Nebula toolkit: PulsarX (fetch) · Quasar (parse) · Photon (identity)