Repository navigation
UUID Generations
This component library provides four related tools for generating unique and/or short-lived identifiers:
| Tool | Purpose | Typical Use |
|---|---|---|
UUID (Uuid, UuidGenerator) |
128-bit globally unique identifiers (RFC 4122) | Database primary keys, distributed IDs, resource identifiers |
ULID (UlidComponent) |
128-bit lexicographically sortable identifiers | Time-ordered primary keys, log correlation, cursor pagination |
OTP (OtpGenerator, OtpCacheManager) |
Short numeric/alphanumeric one-time passwords | Email/SMS verification, passwordless login, transaction confirmation |
TOTP (OtpGenerator::getTotp) |
Time-based one-time passwords (RFC 6238) | Google Authenticator–style 2FA |
All classes are cryptographically secure (using random_bytes / random_int) and designed for modern PHP 8+.
If you use OtpCacheManager, you must also provide a CacheManager implementing get, set, and delete (usually resolved via your container's resolve() helper).
UUID/
├── Random/
│ ├── Uuid.php # Immutable UUID value object
│ └── UuidGenerator.php # Factory for v1, v3, v4
├── ULID/
│ └── UlidComponent.php # ULID generation, parsing, validation
└── OTP/
├── OtpGenerator.php # OTP & TOTP generation/verification
└── OtpCacheManager.php # OTP storage via CacheManager
The simplest and most common UUID:
use Mlangeni\Machinjiri\Core\Components\UUID\Random\UuidGenerator;
$uuid = UuidGenerator::v4();
echo $uuid; // e.g. "f47ac10b-58cc-4372-a567-0e02b2c3d479"$uuid is a Uuid object, and __toString() returns the canonical 8-4-4-4-12 string. You can store it directly in a VARCHAR(36) column.
ULIDs are shorter (26 chars), URL-safe, and sortable by creation time:
use Mlangeni\Machinjiri\Core\Components\UUID\ULID\UlidComponent;
$ulid = UlidComponent::generate();
echo $ulid; // e.g. "01HZX9K3F5J8M2N4P6Q8R0S2T4"Because ULIDs are lexicographically sortable, you can use them as PRIMARY KEY values and ORDER BY id will sort by insertion time.
use Mlangeni\Machinjiri\Core\Components\UUID\OTP\OtpGenerator;
$otp = OtpGenerator::numeric(6);
echo $otp; // e.g. "482913"
⚠️ This only generates a string. To persist and verify it later, see Storing & Verifying OTPs.
| Version | Basis | Deterministic? | When to use |
|---|---|---|---|
| v1 | Timestamp + clock sequence + node | No (but time-ordered) | You need sortable IDs without ULID; legacy systems |
| v3 | MD5(namespace + name) | Yes | Stable IDs for known names (e.g. same email → same UUID) |
| v4 | Pure random (122 bits) | No | Default choice; maximum uniqueness |
use Mlangeni\Machinjiri\Core\Components\UUID\Random\UuidGenerator;
// Version 1 (time-based)
$v1 = UuidGenerator::v1();
// Version 3 (name-based, MD5) — namespace can be a UUID string or object
$v3 = UuidGenerator::v3(
'6ba7b810-9dad-11d1-80b4-00c04fd430c8', // DNS namespace
'example.com'
);
// Version 4 (random)
$v4 = UuidGenerator::v4();
// Or use the generic factory:
$uuid = UuidGenerator::generate(4);
$uuid = UuidGenerator::generate(3, $namespace, 'name');Uuid is an immutable value object. Key methods:
use Mlangeni\Machinjiri\Core\Components\UUID\Random\Uuid;
$uuid = UuidGenerator::v4();
echo $uuid->toString(); // "f47ac10b-58cc-4372-a567-0e02b2c3d479"
echo $uuid->getVersion(); // 4
echo $uuid->getVariant(); // 1 (RFC 4122)
$binary = $uuid->getBytes(); // 16 raw bytes (for BINARY(16) columns)
// Parse a string back to an object:
$parsed = Uuid::fromString('f47ac10b-58cc-4372-a567-0e02b2c3d479');
// From raw binary:
$rebuilt = Uuid::fromBytes($binary);Storing UUIDs as BINARY(16) roughly halves storage vs. VARCHAR(36):
$stmt->bindValue(':id', $uuid->getBytes(), PDO::PARAM_LOB);use Mlangeni\Machinjiri\Core\Components\UUID\ULID\UlidComponent;
$ulid = UlidComponent::generate();
// Validate
if (UlidComponent::validate($ulid)) {
echo "Valid ULID\n";
}
// Parse into components
$parts = UlidComponent::parse($ulid);
echo $parts['timestamp']; // milliseconds since epoch
echo $parts['randomnessHigh']; // 40-bit int
echo $parts['randomnessLow']; // 40-bit int
echo $parts['timestampDateTime']; // DateTimeHandler object
// Extract just the timestamp
$ms = UlidComponent::extractTimestampMs($ulid);
$dt = UlidComponent::extractTimestampDateTime($ulid);This lets you implement cursor pagination using ULIDs:
$lastUlid = $request->get('cursor');
$nextPage = $db->query(
"SELECT * FROM posts WHERE id > :cursor ORDER BY id ASC LIMIT 20",
['cursor' => $lastUlid]
);By default, two ULIDs generated in the same millisecond are randomly ordered. Monotonic ULIDs guarantee increasing order even within the same millisecond:
$ulid1 = UlidComponent::generateMonotonic();
$ulid2 = UlidComponent::generateMonotonic();
// $ulid2 > $ulid1 always (string comparison)This is essential for primary keys and audit logs where tie-breaking must be deterministic.
OtpGenerator supports four types:
use Mlangeni\Machinjiri\Core\Components\UUID\OTP\OtpGenerator;
OtpGenerator::numeric(6); // "042781" (digits only)
OtpGenerator::alphanumeric(8); // "aX9k2Pq1"
OtpGenerator::alphabetic(6); // "KdTqMb"
OtpGenerator::characterBased(5, 'ABCDEF'); // "CABFE" (custom set)Generate an OTP with an expiry timestamp in one call:
$result = OtpGenerator::generateWithExpiry(
length: 6,
type: 'numeric',
ttlSeconds: 300
);
// $result = ['otp' => '482913', 'expires_at' => 1735689600]
// Later:
$isValid = OtpGenerator::verify($userInput, $result['otp'], $result['expires_at']);For real applications, you need to store the OTP so you can verify it later (a plain OtpGenerator::verify() on its own doesn't persist anything). OtpCacheManager does this for you:
use Mlangeni\Machinjiri\Core\Components\UUID\OTP\OtpCacheManager;
use Mlangeni\Machinjiri\Core\Components\UUID\OTP\OtpGenerator;
// The constructor will resolve CacheManager from the container,
// or you can inject one explicitly.
$otpManager = new OtpCacheManager();
// 1. Generate + store
$otp = $otpManager->generateAndStore(
identifier: 'user@example.com',
length: 6,
type: 'numeric',
ttlSeconds: 300
);
// Send $otp to the user (email, SMS, etc.) — NOT back in the HTTP response.
// 2. Later, verify user input
if ($otpManager->verify('user@example.com', $userInput)) {
// Success — but don't forget to invalidate!
$otpManager->delete('user@example.com');
// Proceed with login / passwordless flow
} else {
// Invalid or expired
}
// 3. Optionally, check remaining lifetime
$secondsLeft = $otpManager->getRemainingTtl('user@example.com');🔒 Always
delete()after successful verification. Otherwise an OTP can be replayed until it expires.
UuidGenerator::v1() builds a 60-bit timestamp offset from the Gregorian epoch (1582-10-15) in 100-nanosecond intervals, a random 14-bit clock sequence, and a random 48-bit node with the multicast bit set.
Key detail from the implementation:
$microtime = microtime(true);
$intervals = ($seconds * 10000000) + ($usec * 10) + $gregorianOffset;The tie-breaker — the clock sequence — is regenerated each call, so two v1 UUIDs generated in the same nanosecond may collide only with probability ≈ 1/16384. For heavy generation (thousands per second), consider a monotonic clock sequence (not provided here) or use v4/ULID.
V3 is deterministic:
$ns = Uuid::fromString('6ba7b810-9dad-11d1-80b4-00c04fd430c8'); // DNS
$a = UuidGenerator::v3($ns, 'user@example.com');
$b = UuidGenerator::v3($ns, 'user@example.com');
// $a->toString() === $b->toString() → trueThis is useful for idempotent resource creation: derive the ID from stable input rather than storing a lookup table.
⚠️ MD5 is cryptographically weak. Do not use V3 for security-sensitive uniqueness. For secure name-based IDs, use a v5-style implementation with SHA-1 (not included here) or derive a hash yourself.
A ULID is 128 bits encoded in Crockford's Base32 (no I, L, O, U):
48 bits 80 bits
┌──────────────────┬──────────────────────────────────────┐
│ timestamp (ms) │ randomness │
└──────────────────┴──────────────────────────────────────┘
↑ ↑
10 base32 chars 16 base32 chars
The randomness is split into two 40-bit halves (randomnessHigh, randomnessLow) to avoid integer overflow on 32-bit systems and to simplify encoding.
Encoding (from the source):
$randHighParts = self::intToBase32($randHigh, 8); // 40 bits → 8 chars
$randLowParts = self::intToBase32($randLow, 8); // 40 bits → 8 charsBecause Base32 works in 5-bit groups and 40 ÷ 5 = 8, each half encodes cleanly with no padding. This is a portable and allocation-friendly design.
OtpGenerator includes a full TOTP implementation compatible with Google Authenticator, Authy, 1Password, etc.
Provisioning a new 2FA secret:
use Mlangeni\Machinjiri\Core\Components\UUID\OTP\OtpGenerator;
// 1. Generate a Base32 secret (20 bytes = 160 bits, RFC-recommended)
$secret = OtpGenerator::generateTotpSecret(20);
// → e.g. "JBSWY3DPEHPK3PXPJBSWY3DPEHPK3PXP"
// 2. Show a QR code to the user with this URI:
$label = rawurlencode('alice@example.com');
$issuer = rawurlencode('MyApp');
$uri = "otpauth://totp/{$issuer}:{$label}?secret={$secret}&issuer={$issuer}";
// Render $uri as a QR code (use endroid/qr-code or similar)
// 3. Store $secret (encrypted!) on the user recordVerifying at login:
$userCode = $_POST['totp']; // 6-digit code from Authenticator
$secret = $user->totp_secret; // Base32
if (OtpGenerator::verifyTotp($userCode, $secret)) {
// ✓ Valid (allows ±1 time-window drift = ~±30s)
} else {
// ✗ Invalid or expired
}Direct computation if you need to display the current code (e.g., for tests):
$current = OtpGenerator::getTotp($secret, 30, 6); // 6-digit, 30s windowConfiguration options:
| Parameter | Default | Notes |
|---|---|---|
$timeWindow |
30 s | Must match client (Authenticator uses 30) |
$digits |
6 | 6 or 8 supported |
$drift |
1 | Checks windows t-1, t, t+1
|
| Secret length | 20 bytes (160-bit) | 10–64 bytes allowed |
🔐 Store TOTP secrets encrypted at rest. A leaked secret allows unlimited code generation.
UUIDs & ULIDs
- ULIDs leak creation time in the first 10 characters. Fine for internal IDs; if you expose them publicly, consider truncating or hashing.
- UUID v1 also leaks time (and node/MAC if not randomized; this implementation randomizes the node).
- V4 UUIDs are 122 bits of entropy — collision probability is astronomically low (≈ 1 in 2^61 for a billion UUIDs).
- When storing UUIDs in SQL, prefer
BINARY(16)($uuid->getBytes()). This also avoids case-sensitivity bugs (UUID()vsuuid()).
OTPs
- Rate-limit attempts. A 6-digit OTP has only 1,000,000 combinations; without throttling it's brute-forceable. Track failed attempts and lock after N tries.
-
Use short TTLs (60–300 seconds) and always
delete()after success. - Send OTPs out-of-band (email, SMS) — never return them in the HTTP response body or logs.
-
OtpCacheManager::verify()useshash_equals(constant-time) to prevent timing attacks. ✅ - The identifier is sanitized before being used as a cache key — this is important because email addresses contain
@and., which some cache backends reject. -
Avoid the same OTP for multiple users — the
identifierparameter must be unique per recipient.
TOTP
- Never log or transmit the secret.
- Consider one-time backup codes for users who lose their authenticator.
- Reject reused codes: RFC 6238 recommends that the server remember the last used timestamp per user (not implemented here — add it in your auth layer if strict replay prevention is needed).
Custom OTP storage (e.g., Redis, database):
class RedisOtpStore
{
public function __construct(private \Redis $redis) {}
public function generateAndStore(
string $identifier, int $length, string $type = 'numeric',
int $ttlSeconds = 300, ?string $customChars = null
): string {
$otp = match ($type) {
'numeric' => OtpGenerator::numeric($length),
'alphanumeric' => OtpGenerator::alphanumeric($length),
'alphabetic' => OtpGenerator::alphabetic($length),
'characterBased' => OtpGenerator::characterBased($length, $customChars),
};
$this->redis->setex("otp:{$identifier}", $ttlSeconds, $otp);
return $otp;
}
public function verify(string $identifier, string $otp): bool
{
$stored = $this->redis->get("otp:{$identifier}");
return $stored !== null && hash_equals($stored, $otp);
}
}Dependency injection:
OtpCacheManager accepts an optional CacheManager. In a framework like Laravel or Symfony, bind it explicitly:
$otpManager = new OtpCacheManager(app(CacheManager::class), 'otp:');| Method | Signature | Returns |
|---|---|---|
v1() |
static v1(): Uuid |
Time-based UUID |
v3() |
static v3(Uuid|string $ns, string $name): Uuid |
MD5 name-based UUID |
v4() |
static v4(): Uuid |
Random UUID |
generate() |
static generate(int $version, mixed ...$args): Uuid |
Generic factory |
| Method | Signature | Returns |
|---|---|---|
__construct() |
__construct(string $bytes) |
— (must be 16 bytes) |
fromString() |
static fromString(string $uuid): self |
Parsed UUID |
fromBytes() |
static fromBytes(string $bytes): self |
From binary |
toString() |
toString(): string |
Canonical string |
getBytes() |
getBytes(): string |
16 raw bytes |
getVersion() |
getVersion(): int |
1, 3, 4 |
getVariant() |
getVariant(): int |
0=NCS, 1=RFC, 2=MS, 3=Reserved |
| Method | Signature | Returns |
|---|---|---|
generate() |
generate(?DateTimeHandler $ts = null): string |
ULID string |
generateMonotonic() |
generateMonotonic(?DateTimeHandler $ts = null): string |
Strictly-increasing ULID |
validate() |
validate(string $ulid): bool |
— |
parse() |
parse(string $ulid): array |
[timestamp, randomnessHigh, randomnessLow, timestampDateTime] |
extractTimestampMs() |
extractTimestampMs(string $ulid): int |
Unix ms |
extractTimestampDateTime() |
extractTimestampDateTime(string $ulid): DateTimeHandler |
— |
| Method | Signature | Returns |
|---|---|---|
numeric() |
numeric(int $length): string |
Digits |
alphanumeric() |
alphanumeric(int $length): string |
Letters + digits |
alphabetic() |
alphabetic(int $length): string |
Letters |
characterBased() |
characterBased(int $length, ?string $chars): string |
Custom alphabet |
generateWithExpiry() |
generateWithExpiry(int $length, string $type = 'numeric', int $ttl = 300, ?string $chars = null): array |
['otp'=>..., 'expires_at'=>...] |
verify() |
verify(string $input, string $stored, ?int $expiresAt = null): bool |
— |
generateTotpSecret() |
generateTotpSecret(int $bytes = 20): string |
Base32 string |
getTotp() |
getTotp(string $secret, int $win = 30, int $digits = 6): string |
Current code |
verifyTotp() |
verifyTotp(string $input, string $secret, int $win = 30, int $digits = 6, int $drift = 1): bool |
— |
| Method | Signature | Returns |
|---|---|---|
__construct() |
__construct(?CacheManager $cache = null, string $prefix = 'otp_') |
— |
generateAndStore() |
generateAndStore(string $id, int $length, string $type = 'numeric', int $ttl = 300, ?string $chars = null): string |
The plain-text OTP |
verify() |
verify(string $id, string $otp): bool |
— |
delete() |
delete(string $id): bool |
— |
getRemainingTtl() |
getRemainingTtl(string $id): ?int |
Seconds or null |
| Feature | UUID v4 | UUID v1 | ULID |
|---|---|---|---|
| Length | 36 chars | 36 chars | 26 chars |
| Sortable by time | ❌ | ✅ (roughly) | ✅ |
| Leaks timestamp | ❌ | ✅ | ✅ |
| Case-sensitive | ❌ (hex) | ❌ | ✅ (Base32) |
| URL-safe | ✅ (-) |
✅ | ✅ |
| DB storage | 36 B (or 16 B binary) | 36 B | 26 B |
| Best for | Max randomness | Legacy | Primary keys |
| Type | Alphabet | Bits/char | 6-char entropy | Use |
|---|---|---|---|---|
| Numeric | 0–9 | 3.32 | ~20 bits | SMS, email |
| Alphabetic | A–Z, a–z | 5.7 | ~34 bits | Voice / apps |
| Alphanumeric | 0–9, A–Z, a–z | 5.95 | ~36 bits | Better security |
| Custom | Your set | log₂(n) | varies | Compliance rules |
-
Comparing UUIDs as strings with
==vs===— always use the canonical lowercase form (Uuid::fromString()normalizes to lowercase). -
Forgetting to
delete()an OTP after successful verification — leads to replay attacks. - Returning the OTP in the API response — a huge security hole. Send it out-of-band.
-
Using
generateMonotonic()across multiple processes — the monotonic counter is static per process, so multiple PHP-FPM workers each maintain their own state. Usegenerate()(random) or a shared monotonic source (e.g., RedisINCR) if strict global ordering is required. - Storing TOTP secrets in plaintext — encrypt them with a KMS or libsodium.
-
Trusting the client clock for TOTP — servers should rely on
time()only; the ±1 drift window covers small skews. - Using ULIDs as security tokens — they leak creation time and are predictable in prefix. Use a random token (or UUID v4) for anything secret.
-
32-bit PHP — some ULID paths (like
1 << 40) will overflow. Stick to 64-bit builds; the component was designed for modern 64-bit PHP.
Q: Can I generate a UUID v5 (SHA-1 name-based)?
Not yet — the current UuidGenerator implements v1, v3, and v4. A v5 implementation would look identical to v3 but use sha1(..., true) and set the version nibble to 0x50. You can subclass or add it in your project.
Q: Is UuidGenerator::v1() safe in a cluster?
Partially. Each node uses a random node and random clock sequence on every call, so cross-node collisions are extremely unlikely. For strict RFC compliance (stable node per host), pass a stable node ID — the current implementation does not expose that parameter.
Q: What's the difference between OtpGenerator::verify() and OtpCacheManager::verify()?
OtpGenerator::verify() compares two strings you already have (one supplied, one stored elsewhere — you manage storage). OtpCacheManager::verify() looks up the stored OTP in cache for you, keyed by identifier, and compares using hash_equals.
Q: Does ULID support microsecond precision? The ULID spec is millisecond precision only. Sub-millisecond ordering is handled by the randomness component (and by monotonic generation).
Q: How many digits should my TOTP use? Stick to 6 for maximum client compatibility (Google Authenticator, Authy, etc.). 8 is fine if you control the client too.
Q: What happens if my CacheManager doesn't implement store()->getTtl()?
OtpCacheManager::getRemainingTtl() returns null. Everything else still works.
Machinjiri Docs
- Home
- Learn
- Build
-
Tutorials
- Routing
- Controllers and Middleware
- Views and Templates
- Database ORM
- Eloquent Database ORM
- Errors and Exceptions
- Forms and Validation
- Http
- Events and Logging
- Services and DI
- Security
- Authentication
- Caching
- File System
- Date and Time
- Debugging
- Queue and Jobs
- Task Scheduling
- Transport (Mail, SMS)
- LDAP
- Notifications
- Webhooks
- UUID Generations
- Terminal
- Helper Function
- Reference