RFC 9562 UUID generation as a PHP C extension. Versions 1, 2, 3, 4, 5, 6, 7, 8 plus nil and max, a ramsey/uuid-shaped object API, and procedural functions that skip object allocation. Every example below was run against the built extension; the output shown is what it printed.
# build the extension phpize && ./configure --enable-fast-uuid && make && make test # or with PIE pie install iliaal/fast_uuid # the ramsey-compat layer (optional) ships in this repo's compat/ directory, # not on Packagist; add it as a Composer path repository from your checkout: composer config repositories.fast-uuid-compat path /path/to/fast_uuid/compat composer require iliaal/fast-uuid-compat:@dev
No external libraries. v1 and v6 use an internal RFC-compliant generator with a random node (multicast bit set); v3 and v5 use PHP's bundled MD5 and SHA1.
use FastUuid\Uuid; // generate echo Uuid::uuid4(); // random echo Uuid::uuid7(); // time-ordered, good DB key // parse and read back $u = Uuid::fromString('6ba7b810-9dad-11d1-80b4-00c04fd430c8'); echo $u->getVersion(), ' ', $u->getHex();
36fa4138-9c2e-4765-955e-161592900197 019e8314-69c9-738a-aea4-8a72d37418eb 1 6ba7b8109dad11d180b400c04fd430c8
Each factory returns a FastUuid\Uuid. Cast to string or call toString() for the canonical form.
// time-based and random Uuid::uuid1(); // node + clock, optional Uuid::uuid1($node, $clockSeq) Uuid::uuid6(); // reordered-time, sortable v1 Uuid::uuid4(); // random Uuid::uuid7(); // unix-time-ordered, optional DateTime or unix-ms int
uuid1 60e596ac-5db2-11f1-aea4-a92f0e25fb04 (sample) uuid6 1f15db26-0e59-66ad-aea4-a92f0e25fb04 (sample) uuid4 36fa4138-9c2e-4765-955e-161592900197 (sample) uuid7 019e8314-69c9-738a-aea4-8a72d37418eb (sample)
Live-clock uuid1() and uuid6() share a timestamp high-water mark. When the clock stalls or moves backward, the timestamp advances by 100 ns while supplied node and clock-sequence values stay unchanged. This prevents repeats within one process, or one thread under ZTS. Coordinate fixed node and clock-sequence values across processes, forks, threads, and restarts. Explicit fromDateTime() timestamps do not use this monotonic state.
// name-based (deterministic): v3 = MD5, v5 = SHA1 Uuid::uuid3(Uuid::NAMESPACE_DNS, 'php.net'); Uuid::uuid5(Uuid::NAMESPACE_DNS, 'php.net');
11a38b9a-b3da-360f-9353-a5a725514269 c4a760a8-dbcf-5254-a0d9-6a4474bd1b62
The native signatures are uuid3(object|string $ns, string $name) and uuid5(object|string $ns, string $name). Native UUIDs resolve directly. Other objects resolve through a compatible public, non-static getCore() callable without arguments and returning a native UUID, or through their Stringable string form. Foreign getBytes() methods are not used because their bytes may have GUID or ordered-time storage order. Scalar namespaces follow PHP's ordinary weak string coercion; strict_types=1 rejects non-string scalars with TypeError.
// DCE Security (v2): local id in bytes 0-3, domain in byte 9 $u = Uuid::uuid2(Uuid::DCE_DOMAIN_PERSON, 1000); echo $u->getVersion(), ' ', $u; // v8: caller supplies 16 bytes, version and variant get set echo Uuid::uuid8(hex2bin('00112233445566778899aabbccddeeff'));
2 000003e8-5db2-21f1-b700-5975aec1a10d (local id 1000 = 0x000003e8)
00112233-4455-8677-8899-aabbccddeeff
fromString accepts the canonical form, a urn:uuid: prefix, brace-wrapped, bare 32-hex, and any letter case. A bad string raises FastUuid\Exception\InvalidUuidStringException.
$s = '6ba7b810-9dad-11d1-80b4-00c04fd430c8'; Uuid::fromString($s); Uuid::fromBytes(hex2bin('6ba7b8109dad11d180b400c04fd430c8')); Uuid::fromInteger('143098242404177361603877621312831893704'); Uuid::fromHexadecimal('6ba7b8109dad11d180b400c04fd430c8'); Uuid::isValid($s); // bool
fromString 6ba7b810-9dad-11d1-80b4-00c04fd430c8 fromBytes 6ba7b810-9dad-11d1-80b4-00c04fd430c8 fromInteger 6ba7b810-9dad-11d1-80b4-00c04fd430c8 fromHexadecimal 6ba7b810-9dad-11d1-80b4-00c04fd430c8 isValid true (false for 'nope')
// fromDateTime builds a v1 at the given instant $u = Uuid::fromDateTime(new DateTimeImmutable('2024-01-01 00:00:00')); echo $u->getVersion(), ' ', $u->getDateTime()->format('Y-m-d');
1 2024-01-01
// v7 carries sub-millisecond precision (RFC 9562 method 3): two v7s in the // same millisecond but microseconds apart still sort in time order $utc = new DateTimeZone('UTC'); $a = Uuid::uuid7(new DateTimeImmutable('2024-01-01 00:00:00.000100', $utc)); $b = Uuid::uuid7(new DateTimeImmutable('2024-01-01 00:00:00.000900', $utc)); echo $a, "\n", $b, "\n"; var_dump($a->compareTo($b) < 0); // ordered by the sub-ms fraction // integer-millisecond API: build and read v7 with no DateTime object $u = Uuid::uuid7(1704067200123); echo $u->getTimestampMillis();
018cc251-f400-7199-9088-32b2a89d9aaa (sample; sub-ms 0x199 in the 3rd group) 018cc251-f400-7e66-8fc2-8b98b1b54214 (same ms 018cc251-f400, sub-ms 0xe66, so a < b) bool(true) 1704067200123
Live-clock v7 calls encode the sub-millisecond fraction in rand_a. When the millisecond/sub-millisecond time key repeats or moves backward, the counter in rand_b advances by a fresh CSPRNG-selected value from 1..2^24 inclusive, carrying into the time key on overflow. Single and batch calls share monotonic state within a process, or within a thread under ZTS; ties across processes or threads carry no order. Explicit uuid7($dateTime) and uuid7($unixMillis) calls do not use that state. getDateTime() decodes v7 at millisecond precision.
$u = Uuid::fromString('6ba7b810-9dad-11d1-80b4-00c04fd430c8'); $u->getHex(); $u->getUrn(); $u->getBytes(); // 16 raw bytes $u->getVersion(); $u->getVariant(); $u->getInteger(); $u->getFields(); $u->getDateTime(); $u->getTimestampMillis(); // unix ms, no DateTime object built $u->equals($other); $u->compareTo($other); // ramsey/identifier-style aliases of the four get* converters above $u->toBytes(); $u->toHexadecimal(); $u->toUrn(); $u->toInteger();
getHex 6ba7b8109dad11d180b400c04fd430c8
getUrn urn:uuid:6ba7b810-9dad-11d1-80b4-00c04fd430c8
getVersion 1 getVariant 2
getInteger 143098242404177361603877621312831893704
getFields {"time_low":"6ba7b810","time_mid":"9dad","time_hi_and_version":"11d1",
"clock_seq_hi_and_reserved":"80","clock_seq_low":"b4","node":"00c04fd430c8"}
compareTo -1 (00..01 vs 00..02)
toBytes(), toHexadecimal(), toUrn() and toInteger() are aliases of getBytes(), getHex(), getUrn() and getInteger(), following the get*→to* naming of the newer ramsey/identifier library.
Native equals() and compareTo() use the same object resolution as uuid3() and uuid5(): native UUID, compatible getCore() result, then string form. They also accept UUID strings directly.
getDateTime() works for the time-based versions (1, 2, 6, 7) and raises UnsupportedOperationException for the others. The object is JsonSerializable and Stringable, so it drops straight into json_encode:
echo json_encode(['id' => $u]);
{"id":"6ba7b810-9dad-11d1-80b4-00c04fd430c8"}
These return a string with no object allocation. Use them on the hot path: ORM inserts, cache keys, bulk generation. The _bin forms return the raw 16-byte value with no canonical formatting, and the _batch forms build $n at once and return them as an array. Batch calls are capped at 100,000 UUIDs, and fast_uuid_random_bytes() is capped at 16 MiB.
UUIDv7 batches read the clock once per call and advance the same random-increment counter as single calls. A large batch's timestamps can therefore trail the wall clock by the batch duration.
uuid_v1(); uuid_v3($ns, $name); uuid_v4(); uuid_v5($ns, $name); uuid_v6(); uuid_v7(); uuid_v8($bytes); uuid_v7_at($unixMillis); // v7 from a unix-ms int, no DateTime uuid_v4_fast(); // non-crypto xoshiro256**, IDs only uuid_v1_bin(); uuid_v4_bin(); uuid_v6_bin(); uuid_v7_bin(); // raw 16 bytes, no string uuid_v4_fast_bin(); uuid_v3_bin($ns, $name); uuid_v5_bin($ns, $name); uuid_v8_bin($bytes); uuid_v7_at_bin($unixMillis); uuid_v4_batch($n); uuid_v7_batch($n); // array of $n canonical strings uuid_v4_bin_batch($n); uuid_v7_bin_batch($n); // array of $n raw 16-byte values uuid_to_bin($s); // canonical -> 16 bytes uuid_from_bin($bytes); // 16 bytes -> canonical uuid_is_valid($s); fast_uuid_random_bytes(16); // batched CSPRNG bytes
uuid_v4 fbc41d54-b200-43d8-891c-b9c1a92df715 (sample)
uuid_to_bin 6ba7b8109dad11d180b400c04fd430c8 (hex of bytes)
uuid_from_bin 6ba7b810-9dad-11d1-80b4-00c04fd430c8
uuid_is_valid true
uuid_v4_fast() uses a non-cryptographic PRNG. Don't use it for session tokens, password-reset keys, or anything an attacker shouldn't predict. Use uuid_v4() for those.The FastUuid\Compat package puts a ramsey/uuid-shaped API over the C core. Migration is largely a use swap from Ramsey\Uuid\Uuid to FastUuid\Compat\Uuid. Generation stays on the C fast path unless a custom provider or generator is configured, and compat Uuid::uuid7() accepts the same unix-millisecond int path as the core API.
Compatibility covers the API shape; generation algorithms and some codec conventions differ. Live-clock v7 uses sub-millisecond time and the random increments described above. GUID text follows the canonical RFC form; only its binary storage order is mixed-endian.
Compat uuid3() and uuid5() retain UuidInterface|string namespace parameters. String namespaces identify network-order UUIDs independently of the output codec, including COMB codecs. Subclasses inheriting fromString() behave the same way. Overriding getCodec() does not change namespace identity; an explicit fromString() override retains the subclass parsing path. To use codec-formatted text as a namespace, first decode it with $factory->fromString($text) and pass the resulting UUID object. Objects resolve through a compatible optional getCore() returning a native UUID, then through their string form.
use FastUuid\Compat\Uuid; // factories return the per-version class Uuid::uuid4() instanceof FastUuid\Compat\Rfc4122\UuidV4; // true // Fields and Type objects $u = Uuid::fromString('6ba7b810-9dad-11d1-80b4-00c04fd430c8'); (string) $u->getFields()->getNode(); // Type\Hexadecimal (string) $u->getInteger(); // Type\Integer
instanceof UuidV4 true getNode 00c04fd430c8 getInteger 143098242404177361603877621312831893704
use FastUuid\Compat\Codec\OrderedTimeCodec; use FastUuid\Compat\Guid\Guid; use FastUuid\Compat\UuidFactory; use FastUuid\Compat\Provider\RandomGeneratorInterface; // codec round-trip (MySQL ordered-time layout) $c = new OrderedTimeCodec(); $c->decodeBytes($c->encodeBinary($v1))->equals($v1); // true // GUID mixed-endian presentation (new Guid($u))->toString(); // swap the random source on a factory $f = new UuidFactory(); $f->setRandomGenerator(new class implements RandomGeneratorInterface { public function generate(int $length): string { return random_bytes($length); } }); $f->uuid4()->getVersion(); // 4
codec round-trip true guid 6ba7b810-9dad-11d1-80b4-00c04fd430c8 factory uuid4 ver 4
fromString() and fromBytes() honor the active codec. With GuidStringCodec, pass canonical text to fromString() and mixed-endian GUID bytes to fromBytes(). fromHexadecimal() and fromInteger() always read network-order identity and attach the codec for presentation only.
These measurements predate the v1/v6 timestamp guard and the v7 random-increment fix. They are historical results, not measurements of the current generation paths.
Throughput against ramsey/uuid 4.9.2 and the PECL uuid extension 1.3.0, on PHP 8.4.22 NTS (non-debug, x86-64 with the SSSE3 formatter). Best of 40 runs, 300k iterations each. Each engine ran in its own process. Higher is faster. The fast_uuid operations run in ~50 ns, so scheduler noise gives the peak roughly ±10% run-to-run variance; ramsey and PECL reproduce to ~3%.
Procedural and PECL paths return a string directly; the object and ramsey paths build an object then format. Compare procedural to PECL and object to ramsey for like-for-like.
| Operation | fast_uuid (object) | fast_uuid (procedural) |
|---|---|---|
| v4 | 11.5x | 17.7x |
| v1 | 42x | 57x |
| v7 | 18.3x | 30x |
| parse | 7.2x | 11.3x |
The batched CSPRNG drives the v4 gap: one syscall covers many UUIDs instead of one each, and on CPUs with AES instructions an AES-256-CTR generator produces the buffer, reseeding from getrandom() every 64 KiB. PECL uuid keeps pace on v1 but its v4 draws fresh entropy per call, so it falls to 0.47M.
The uuid_v4_batch($n) / uuid_v7_batch($n) functions build $n UUIDs in one call and return them as an array, amortizing the per-call setup. The _bin_batch forms return raw 16-byte strings and skip canonical formatting. Per-UUID throughput at a batch size of 100 (single-call shown for reference):
| Operation | fast_uuid (procedural) | vs single-call |
|---|---|---|
uuid_v4() (single) | 19.5M | n/a |
uuid_v4_batch | 22.5M | +15% |
uuid_v4_bin_batch | 25M | +28% |
uuid_v7() (single) | 19.8M | n/a |
uuid_v7_batch | 25M | +26% |
uuid_v7_bin_batch | 29M | +47% |
Single-call binary forms (uuid_v4_bin(), uuid_v7_bin(), uuid_v3_bin(), uuid_v5_bin()) return the same raw bytes without formatting; generation is CSPRNG-bound, so on one call they run within noise of their string counterparts. The win is in the batch and in avoiding a format-then-reparse round trip when the caller wants bytes.
Building v7 from an explicit time, and reading a UUID's timestamp back, read and write ext/date's internal timelib_time directly instead of routing through call_user_function (about 3x faster), and an integer-millisecond API skips DateTime objects entirely. Same machine; ramsey/uuid 4.9.2.
| Operation | fast_uuid (object) | fast_uuid (procedural) | ramsey/uuid |
|---|---|---|---|
| v7 from DateTime | 11.9M | n/a | 0.68M |
| v7 from unix-ms int | 13.4M | 21.2M | n/a |
| fromDateTime (v1) | 12.5M | n/a | n/a |
| getDateTime (read) | 4.4M | n/a | 0.10M |
| getTimestampMillis (read) | n/a | 24.0M | n/a |
uuid7($dateTime) runs ~17x faster than ramsey's and getDateTime() ~44x. Uuid::uuid7($unixMillis), uuid_v7_at($unixMillis), and getTimestampMillis() are integer-native with no DateTime object, and ramsey has no equivalent. Full method and raw numbers live in BENCHMARKS.md.