alesitom / hybrid-id
Compact, time-sortable unique ID generator. A space-efficient alternative to UUID with configurable entropy profiles.
Installs: 4
Dependents: 0
Suggesters: 0
Security: 0
Stars: 0
Watchers: 0
Forks: 0
Open Issues: 0
pkg:composer/alesitom/hybrid-id
Requires
- php: >=8.3
Requires (Dev)
- phpunit/phpunit: ^11.0
Suggests
- vlucas/phpdotenv: Load HYBRID_ID_PROFILE and HYBRID_ID_NODE from .env files (^5.6)
- dev-main
- v2.0.0
- v1.4.1
- v1.4.0
- v1.3.0
- v1.2.0
- v1.1.2
- v1.1.1
- v1.1.0
- v1.0.0
- dev-docs/v2-readme
- dev-feat/v2-custom-profiles
- dev-feat/v2-compare-method
- dev-feat/v2-random-bytes-optimization
- dev-feat/v2-instance-api
- dev-feat/v2-prefix-support
- dev-dependabot/github_actions/actions/checkout-6.0.2
- dev-dependabot/github_actions/shivammathur/setup-php-44454db4f0199b8b9685a5d763dc37cbf79108e1
- dev-fix/packagist-auth
- dev-fix/v1.4.0-polish
- dev-fix/v1.3.0-refinements
- dev-fix/v1.2.0-hardening
- dev-fix/packagist-auto-update
- dev-fix/drop-php82
- dev-fix/v1.1.0-improvements
This package is auto-updated.
Last update: 2026-02-14 23:32:08 UTC
README
Compact, time-sortable unique ID generator for PHP. A space-efficient alternative to UUID with configurable entropy profiles, Stripe-style prefixes, and an instance-based API.
Why HybridId?
| Feature | UUID v4 | UUID v7 | ULID | Snowflake | HybridId |
|---|---|---|---|---|---|
| Length | 36 chars | 36 chars | 26 chars | 18-19 digits | 16-24+ chars |
| Time-sortable | No | Yes | Yes | Yes | Yes |
| URL-safe | No (hyphens) | No (hyphens) | Yes | Yes | Yes (base62) |
| Human-readable | Low | Low | Medium | Low | High |
| Self-documenting | No | No | No | No | Yes (prefixes) |
| Multi-node safe | Yes | Yes | No | Yes (node bits) | Yes (node chars) |
| Configurable size | No | No | No | No | Yes (profiles) |
| Random entropy | 122 bits | 74 bits | 80 bits | 12 bits | 35.7 - 83.4+ bits |
| Dependencies | None | None | Library | Library | None |
Installation
composer require alesitom/hybrid-id
Requires PHP >= 8.3 (64-bit). No external dependencies.
Quick Start
use HybridId\HybridIdGenerator; $gen = new HybridIdGenerator(); $id = $gen->generate(); // 0VBFDQz4CYRtntu09sbf $id = $gen->generate('usr'); // usr_0VBFDQz4CYRtntu09sbf
Profiles
Three built-in profiles with different size/entropy tradeoffs:
| Profile | Length | Structure | Random entropy | vs UUID v7 (74 bits) |
|---|---|---|---|---|
compact |
16 | 8ts + 2node + 6rand | 35.7 bits | Lower |
standard |
20 | 8ts + 2node + 10rand | 59.5 bits | Comparable |
extended |
24 | 8ts + 2node + 14rand | 83.4 bits | Higher |
Structure breakdown:
0VBFDQz4 CY Rtntu09sbf
|______| |_| |_________|
ts node random
- ts (8 chars): Millisecond timestamp in base62. Enables chronological sorting. Covers ~6,920 years from epoch.
- node (2 chars): Server/process identifier. Prevents cross-node collisions.
- rand (variable): Cryptographically secure random bytes. Prevents same-millisecond collisions.
Creating a Generator
Via constructor
use HybridId\HybridIdGenerator; // Default: standard profile, auto-detected node $gen = new HybridIdGenerator(); // Explicit profile and node $gen = new HybridIdGenerator(profile: 'extended', node: 'A1');
Via environment variables
$gen = HybridIdGenerator::fromEnv();
Reads from:
HYBRID_ID_PROFILE=standard HYBRID_ID_NODE=A1
For .env file support, install vlucas/phpdotenv:
composer require vlucas/phpdotenv
$dotenv = Dotenv\Dotenv::createImmutable(__DIR__); $dotenv->load(); $gen = HybridIdGenerator::fromEnv();
Node auto-detection
When no node is provided, the generator derives a deterministic 2-char identifier from gethostname() and getmypid(). For multi-server deployments, set an explicit node per instance to guarantee uniqueness.
Generating IDs
$gen = new HybridIdGenerator(); // Using the instance's configured profile (default: standard) $id = $gen->generate(); // 0VBFDQz4CYRtntu09sbf // Explicit profile methods $id = $gen->compact(); // 0VBFDQz4CY8xegI0 $id = $gen->standard(); // 0VBFDQz5CYS0PQgr0sbf $id = $gen->extended(); // 0VBFDQz6CYxiF0G9pBKVwwn2
Prefixes
Stripe-style prefixes make IDs self-documenting. Pass an optional prefix to any generation method:
$gen = new HybridIdGenerator(); $id = $gen->generate('usr'); // usr_0VBFDQz4CYRtntu09sbf $id = $gen->generate('ord'); // ord_0VBFDQz5CYxiF0G9pBKV $id = $gen->compact('log'); // log_0VBFDQz6CY8xegI0 $id = $gen->extended('txn'); // txn_0VBFDQz7CYpBKVwwn2xiF0
Prefix rules:
- 1 to 8 characters
- Lowercase alphanumeric only, must start with a letter
- Separated by underscore (
_) - Optional: omit for unprefixed IDs
All extraction and validation methods handle prefixed IDs transparently.
Multiple Generators
Each instance has its own profile, node, and monotonic counter. Use multiple generators for different entity types:
$userIds = new HybridIdGenerator(profile: 'extended', node: 'U1'); $logIds = new HybridIdGenerator(profile: 'compact', node: 'L1'); $userId = $userIds->generate('usr'); // usr_... (24 char ID) $logId = $logIds->generate('log'); // log_... (16 char ID)
Instance state is fully independent -- different monotonic counters, different nodes, no cross-contamination.
Validation
use HybridId\HybridIdGenerator; HybridIdGenerator::isValid('0VBFDQz4CYRtntu09sbf'); // true HybridIdGenerator::isValid('usr_0VBFDQz4CYRtntu09sbf'); // true HybridIdGenerator::isValid('invalid'); // false HybridIdGenerator::detectProfile('0VBFDQz4CYRtntu09sbf'); // "standard" HybridIdGenerator::detectProfile('usr_0VBFDQz4CY8xegI0'); // "compact" HybridIdGenerator::detectProfile('bad'); // null
Extracting Metadata
Every HybridId encodes its creation time, generating node, and optional prefix:
use HybridId\HybridIdGenerator; $gen = new HybridIdGenerator(node: 'A1'); $id = $gen->generate('usr'); // usr_0VBFDQz4A1Rtntu09sbf HybridIdGenerator::extractTimestamp($id); // 1771109611324 (ms since epoch) HybridIdGenerator::extractDateTime($id); // DateTimeImmutable (2026-02-14 22:53:31.324) HybridIdGenerator::extractNode($id); // "A1" HybridIdGenerator::extractPrefix($id); // "usr" HybridIdGenerator::extractPrefix($gen->generate()); // null (no prefix)
Sorting
Compare IDs chronologically with compare(), compatible with usort():
use HybridId\HybridIdGenerator; $gen = new HybridIdGenerator(); $ids = []; for ($i = 0; $i < 100; $i++) { $ids[] = $gen->generate(); } shuffle($ids); // Sort chronologically usort($ids, HybridIdGenerator::compare(...)); // Works with prefixed IDs too -- prefixes are stripped before comparison $mixed = [$gen->generate('usr'), $gen->generate('ord'), $gen->generate('log')]; usort($mixed, HybridIdGenerator::compare(...));
Introspection
use HybridId\HybridIdGenerator; HybridIdGenerator::entropy('compact'); // 35.7 HybridIdGenerator::entropy('standard'); // 59.5 HybridIdGenerator::entropy('extended'); // 83.4 HybridIdGenerator::profiles(); // ['compact', 'standard', 'extended'] HybridIdGenerator::profileConfig('compact'); // ['length' => 16, 'ts' => 8, 'node' => 2, 'random' => 6] $gen = new HybridIdGenerator(profile: 'extended', node: 'A1'); $gen->getProfile(); // "extended" $gen->getNode(); // "A1"
Custom Profiles
Register profiles with custom random lengths. Timestamp (8) and node (2) are fixed -- only the random portion is configurable:
use HybridId\HybridIdGenerator; // Register a 32-char profile: 8ts + 2node + 22random HybridIdGenerator::registerProfile('ultra', 22); $gen = new HybridIdGenerator(profile: 'ultra'); $id = $gen->generate('txn'); strlen($id); // 36 (3 prefix + 1 underscore + 32) HybridIdGenerator::detectProfile($id); // "ultra" HybridIdGenerator::entropy('ultra'); // 130.9 // Register a minimal 12-char profile: 8ts + 2node + 2random HybridIdGenerator::registerProfile('tiny', 2);
Constraints:
- Profile name must be lowercase alphanumeric, starting with a letter
- Random length must be at least 1
- Total length must not conflict with an existing profile
Interface and Dependency Injection
HybridIdGenerator implements the IdGenerator interface for clean DI and testing:
use HybridId\IdGenerator; use HybridId\HybridIdGenerator; class UserService { public function __construct( private readonly IdGenerator $idGenerator, ) {} public function createUser(string $name): User { return new User( id: $this->idGenerator->generate('usr'), name: $name, ); } } // Production $service = new UserService(new HybridIdGenerator(profile: 'extended', node: 'A1')); // Testing $mock = $this->createMock(IdGenerator::class); $mock->method('generate')->willReturn('usr_testid12345678AB'); $service = new UserService($mock);
The interface:
interface IdGenerator { public function generate(?string $prefix = null): string; }
CLI
# Generate IDs ./vendor/bin/hybrid-id generate # 0VBFDQz4CYRtntu09sbf ./vendor/bin/hybrid-id generate -p compact -n 5 # 0VBFDQz4CY8xegI0 # 0VBFDQz5CYRtntu0 # 0VBFDQz6CY9jLlWd # 0VBFDQz7CYDexq1t # 0VBFDQz8CY8beN74 ./vendor/bin/hybrid-id generate --prefix usr --node A1 # usr_0VBFDQz4A1Rtntu09sbf ./vendor/bin/hybrid-id generate -p extended --prefix txn -n 3 # txn_0VBFDQz4CYxiF0G9pBKVwwn2 # txn_0VBFDQz5CYS0PQgr0sbfAbCd # txn_0VBFDQz6CYRtntu09sbfXyZw
# Inspect an existing ID ./vendor/bin/hybrid-id inspect usr_0VBFDQz4A1Rtntu09sbf # ID: usr_0VBFDQz4A1Rtntu09sbf # Prefix: usr # Profile: standard (20 chars) # Timestamp: 1771109611324 # DateTime: 2026-02-14 22:53:31.324 # Node: A1 # Random: Rtntu09sbf # Entropy: 59.5 bits # Valid: yes
# Show all profiles ./vendor/bin/hybrid-id profiles # Profile Length Structure Random bits vs UUID v7 # ------- ------ --------- ----------- ---------- # compact 16 8ts + 2node + 6rand 35.7 bits < UUID v7 # standard 20 8ts + 2node + 10rand 59.5 bits ~ UUID v7 # extended 24 8ts + 2node + 14rand 83.4 bits > UUID v7
Database Usage
-- Standard profile (unprefixed) CREATE TABLE users ( id CHAR(20) NOT NULL PRIMARY KEY, ... ); -- Compact profile (unprefixed) CREATE TABLE logs ( id CHAR(16) NOT NULL PRIMARY KEY, ... ); -- With prefixes: use VARCHAR to accommodate prefix + underscore CREATE TABLE orders ( id VARCHAR(29) NOT NULL PRIMARY KEY, -- up to 8 prefix + 1 underscore + 20 ID ... );
Why this works well with B-tree indexes:
- Chronological ordering means sequential inserts, reducing page splits
- Smaller than CHAR(36) UUIDs, improving index density and JOIN performance
- Base62 encoding is safe for any column collation
Choosing a Profile
compact(16 chars): Internal PKs, low-scale apps, storage-constrained systems. ~35.7 bits entropy means 50% collision probability at ~236,000 IDs per millisecond per node. Not recommended for high-throughput multi-node systems.standard(20 chars): General purpose, recommended default. ~59.5 bits provides comfortable collision resistance for most applications.extended(24 chars): High-scale, public-facing IDs, when you need more entropy than UUID v7. ~83.4 bits of random entropy.- Custom profiles: Use
registerProfile()when built-in profiles don't match your requirements.
Security Considerations
Not for secrets. Do NOT use HybridId for security tokens, password resets, API keys, or session tokens. The timestamp is predictable and reduces effective entropy. Use random_bytes() with 128+ bits of pure entropy for those.
Timestamp disclosure. The first 8 characters encode the creation time to the millisecond. Anyone with a HybridId can extract when it was created and which node generated it. This is inherent to the design (same as UUID v7). Do not use HybridId where creation time must be confidential.
Validation is not constant-time. isValid() returns early on the first invalid character. If you compare HybridIds in security-sensitive contexts (e.g., authorization), use hash_equals() instead of === to prevent timing side-channels.
Clock Drift Protection
Each generator instance maintains a monotonic guard that ensures timestamps never go backward and strictly increment even within the same millisecond. If the system clock moves backward (NTP adjustment), or multiple IDs are generated in the same millisecond, the timestamp increments by 1ms to guarantee strict chronological ordering.
Concurrency and Limitations
Per-instance scope. The monotonic guard is scoped to each HybridIdGenerator instance. In PHP-FPM or mod_php, each request creates its own instance with independent state. Two concurrent requests in the same millisecond on the same node rely on the random component for uniqueness.
Async runtimes. In long-running processes (Swoole, ReactPHP, Amphp), a shared instance maintains monotonic ordering within the process. The guard works correctly under cooperative scheduling (PHP Fibers), but has no atomicity guarantees under preemptive coroutines.
Node auto-detection. The auto-detected node is derived from gethostname() and getmypid() via crc32(), reduced to 3,844 possible values. In clustered deployments with many processes, always set the node explicitly to guarantee uniqueness.
Upgrading from v1.x
v2.0.0 replaces the static HybridId class with an instance-based HybridIdGenerator. There is no backward compatibility layer.
API changes
| v1.x | v2.0.0 |
|---|---|
use HybridId\HybridId |
use HybridId\HybridIdGenerator |
HybridId::configure(['profile' => 'compact', 'node' => 'A1']) |
new HybridIdGenerator(profile: 'compact', node: 'A1') |
HybridId::generate() |
$gen->generate() |
HybridId::compact() |
$gen->compact() |
HybridId::standard() |
$gen->standard() |
HybridId::extended() |
$gen->extended() |
HybridId::configureFromEnv() |
HybridIdGenerator::fromEnv() |
HybridId::reset() |
Create a new instance |
HybridId::entropy() (no args) |
HybridIdGenerator::entropy('standard') (required arg) |
HybridId::profileConfig() (no args) |
HybridIdGenerator::profileConfig('standard') (required arg) |
Static utilities (unchanged pattern)
These methods remain static and work the same way, just on HybridIdGenerator:
HybridIdGenerator::isValid($id); HybridIdGenerator::detectProfile($id); HybridIdGenerator::extractTimestamp($id); HybridIdGenerator::extractDateTime($id); HybridIdGenerator::extractNode($id); HybridIdGenerator::profiles();
New in v2.0.0
- Prefixes:
$gen->generate('usr')producesusr_... - Instance-based API: Multiple generators with independent state
IdGeneratorinterface: For dependency injection and testingcompare(): Chronological sorting withusort()- Custom profiles:
registerProfile('ultra', 22) fromEnv(): Named constructor from environment variablesgetProfile()/getNode(): Instance getters
ID format
The generated ID format is identical between v1.x and v2.0.0. IDs generated by v1.x are fully valid and readable by v2.0.0 utilities.
Requirements
- PHP >= 8.3 (64-bit)
- No external dependencies
License
MIT