Skip to content

Commit f78ee83

Browse files
authored
Add UUIDv8, change UUIDv7 to millisecond precision (#63)
* Add UUIDv8, change UUIDv7 to millisecond precision
1 parent bfc16ef commit f78ee83

File tree

5 files changed

+191
-32
lines changed

5 files changed

+191
-32
lines changed

README.md

Lines changed: 50 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,12 @@
33

44
# uuid-php
55

6-
A small PHP class for generating [RFC 4122][RFC 4122] version 3, 4, and 5 universally unique identifiers (UUID). Additionally supports [draft][draft 04] versions 6 and 7.
6+
A small PHP class for generating [RFC 4122][RFC 4122] version 3, 4, and 5 universally unique identifiers (UUID). Additionally supports [draft][draft] versions 6, 7, and 8.
77

88
If all you want is a unique ID, you should call `uuid4()`.
99

10+
> Implementations SHOULD utilize UUID version 7 over UUID version 1 and 6 if possible.
11+
1012
## Minimal UUID v4 implementation
1113

1214
Credits go to [this answer][stackoverflow uuid4] on Stackoverflow for this minimal RFC 4122 compliant solution.
@@ -25,7 +27,7 @@ echo uuid4();
2527

2628
## Installation
2729

28-
If you need comparison tools or sortable identifiers like in versions 6 and 7, you might find this small and fast package useful. It doesn't require any other dependencies.
30+
If you need comparison tools or sortable identifiers like in versions 6, 7, and 8, you might find this small and fast package useful. It doesn't require any other dependencies.
2931

3032
```bash
3133
composer require oittaa/uuid
@@ -64,6 +66,12 @@ echo $uuid7_first . "\n"; // e.g. 017f22e2-79b0-7cc3-98c4-dc0c0c07398f
6466
$uuid7_second = UUID::uuid7();
6567
var_dump($uuid7_first < $uuid7_second); // bool(true)
6668

69+
// Generate a version 8 (lexicographically sortable) UUID
70+
$uuid8_first = UUID::uuid8();
71+
echo $uuid8_first . "\n"; // e.g. 017f22e2-79b0-8cc3-98c4-dc0c0c07398f
72+
$uuid8_second = UUID::uuid8();
73+
var_dump($uuid8_first < $uuid8_second); // bool(true)
74+
6775
// Test if a given string is a valid UUID
6876
$isvalid = UUID::isValid('11a38b9a-b3da-360f-9353-a5a725514269');
6977
var_dump($isvalid); // bool(true)
@@ -111,19 +119,54 @@ $cmp3 = UUID::cmp(
111119
);
112120
var_dump($cmp3 === 0); // bool(true)
113121

114-
// Extract Unix time from versions 6 and 7 as a string.
122+
// Extract Unix time from versions 6, 7, and 8 as a string.
115123
$uuid6_time = UUID::getTime('1ec9414c-232a-6b00-b3c8-9e6bdeced846');
116124
var_dump($uuid6_time); // string(18) "1645557742.0000000"
117125
$uuid7_time = UUID::getTime('017f22e2-79b0-7cc3-98c4-dc0c0c07398f');
118-
var_dump($uuid7_time); // string(18) "1645557742.0007977"
126+
var_dump($uuid7_time); // string(18) "1645557742.0000000"
127+
$uuid8_time = UUID::getTime('017f22e2-79b0-8cc3-98c4-dc0c0c07398f');
128+
var_dump($uuid8_time); // string(18) "1645557742.0007977"
119129

120130
// Extract the UUID version.
121131
$uuid_version = UUID::getVersion('2140a926-4a47-465c-b622-4571ad9bb378');
122132
var_dump($uuid_version); // int(4)
123133
```
124134

135+
## UUIDv6 Field and Bit Layout
136+
137+
```
138+
0 1 2 3
139+
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
140+
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
141+
| time_high |
142+
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
143+
| time_mid | time_low_and_version |
144+
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
145+
|clk_seq_hi_res | clk_seq_low | node (0-1) |
146+
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
147+
| node (2-5) |
148+
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
149+
```
150+
125151
## UUIDv7 Field and Bit Layout
126152

153+
```
154+
155+
0 1 2 3
156+
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
157+
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
158+
| unix_ts_ms |
159+
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
160+
| unix_ts_ms | ver | rand_a |
161+
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
162+
|var| rand_b |
163+
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
164+
| rand_b |
165+
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
166+
```
167+
168+
## UUIDv8 Field and Bit Layout
169+
127170
```
128171
0 1 2 3
129172
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
@@ -139,14 +182,14 @@ var_dump($uuid_version); // int(4)
139182
```
140183

141184
- `unix_ts_ms`: 48 bit big-endian unsigned number of Unix epoch timestamp with millisecond level of precision
142-
- `ver`: The 4 bit UUIDv7 version (0111)
185+
- `ver`: The 4 bit UUIDv8 version (1000)
143186
- `subsec`: 12 bits allocated to sub-second precision values
144187
- `var`: 2 bit UUID variant (10)
145188
- `sub`: 2 bits allocated to sub-second precision values
146189
- `rand`: The remaining 60 bits are filled with pseudo-random data
147190

148-
14 bits dedicated to sub-second precision provide 100 nanosecond resolution. The `unix_ts` and `subsec` fields guarantee the order of UUIDs generated within the same timestamp by monotonically incrementing the timer.
191+
14 bits dedicated to sub-second precision provide 100 nanosecond resolution. The `unix_ts_ms` and `subsec` fields guarantee the order of UUIDs generated within the same timestamp by monotonically incrementing the timer.
149192

150193
[RFC 4122]: http://tools.ietf.org/html/rfc4122
151-
[draft 04]: https://datatracker.ietf.org/doc/html/draft-peabody-dispatch-new-uuid-format-04
194+
[draft]: https://github.com/ietf-wg-uuidrev/rfc4122bis
152195
[stackoverflow uuid4]: https://stackoverflow.com/a/15875555

src/UUID.php

Lines changed: 76 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,13 @@
88
* Represents a universally unique identifier (UUID), according to RFC 4122.
99
*
1010
* This class provides the static methods `uuid3()`, `uuid4()`, `uuid5()`,
11-
* `uuid6()`, and `uuid7()` for generating version 3, 4, 5, 6 (draft), and
12-
* 7 (draft) UUIDs.
11+
* `uuid6()`, `uuid7()`, and `uuid8()` for generating version 3, 4, 5,
12+
* 6 (draft), 7 (draft), and 8 (draft) UUIDs.
1313
*
1414
* If all you want is a unique ID, you should call `uuid4()`.
1515
*
1616
* @link http://tools.ietf.org/html/rfc4122
17-
* @link https://github.com/uuid6/uuid6-ietf-draft
17+
* @link https://github.com/ietf-wg-uuidrev/rfc4122bis
1818
* @link http://en.wikipedia.org/wiki/Universally_unique_identifier
1919
*/
2020
class UUID
@@ -49,6 +49,12 @@ class UUID
4949
* @link http://tools.ietf.org/html/rfc4122#section-4.1.7
5050
*/
5151
public const NIL = '00000000-0000-0000-0000-000000000000';
52+
/**
53+
* The Max UUID is special form of UUID that is specified to have all 128 bits set to one.
54+
* @var string
55+
* @link https://www.ietf.org/archive/id/draft-ietf-uuidrev-rfc4122bis-00.html#name-max-uuid
56+
*/
57+
public const MAX = 'FFFFFFFF-FFFF-FFFF-FFFF-FFFFFFFFFFFF';
5258

5359
/**
5460
* 0x01b21dd213814000 is the number of 100-ns intervals between the
@@ -65,7 +71,10 @@ class UUID
6571
private const V7_SUBSEC_RANGE = 10_000;
6672

6773
/** @internal */
68-
private const V7_SUBSEC_BITS = 14;
74+
private const V8_SUBSEC_RANGE = 10_000;
75+
76+
/** @internal */
77+
private const V8_SUBSEC_BITS = 14;
6978

7079
/** @internal */
7180
private const UUID_REGEX = '/^(?:urn:)?(?:uuid:)?(\{)?([0-9a-f]{8})\-?([0-9a-f]{4})'
@@ -78,7 +87,10 @@ class UUID
7887
private static $subsec = 0;
7988

8089
/** @internal */
81-
private static function getUnixTime(): array
90+
private static $unixts_ms = 0;
91+
92+
/** @internal */
93+
private static function getUnixTimeSubsec(): array
8294
{
8395
$timestamp = microtime(false);
8496
$unixts = intval(substr($timestamp, 11), 10);
@@ -98,6 +110,19 @@ private static function getUnixTime(): array
98110
return [$unixts, $subsec];
99111
}
100112

113+
/** @internal */
114+
private static function getUnixTimeMs(): int
115+
{
116+
$timestamp = microtime(false);
117+
$unixts = intval(substr($timestamp, 11), 10);
118+
$unixts_ms = $unixts * 1000 + intval(substr($timestamp, 2, 3), 10);
119+
if (self::$unixts_ms >= $unixts_ms) {
120+
$unixts_ms = self::$unixts_ms + 1;
121+
}
122+
self::$unixts_ms = $unixts_ms;
123+
return $unixts_ms;
124+
}
125+
101126
/** @internal */
102127
private static function stripExtras(string $uuid): string
103128
{
@@ -138,13 +163,13 @@ private static function uuidFromHex(string $uhex, int $version): string
138163
/** @internal */
139164
private static function encodeSubsec(int $value): int
140165
{
141-
return intdiv($value << self::V7_SUBSEC_BITS, self::V7_SUBSEC_RANGE);
166+
return intdiv($value << self::V8_SUBSEC_BITS, self::V8_SUBSEC_RANGE);
142167
}
143168

144169
/** @internal */
145170
private static function decodeSubsec(int $value): int
146171
{
147-
return -(-$value * self::V7_SUBSEC_RANGE >> self::V7_SUBSEC_BITS);
172+
return -(-$value * self::V8_SUBSEC_RANGE >> self::V8_SUBSEC_BITS);
148173
}
149174

150175
/**
@@ -189,15 +214,16 @@ public static function uuid5(string $namespace, string $name): string
189214
}
190215

191216
/**
192-
* Generate a version 6 UUID. A v6 UUID is lexicographically sortable and contains
193-
* a 60-bit timestamp and 62 extra unique bits. Unlike version 1 UUID, this
194-
* implementation of version 6 UUID doesn't leak the MAC address of the host.
217+
* UUID version 6 is a field-compatible version of UUIDv1, reordered for improved
218+
* DB locality. It is expected that UUIDv6 will primarily be used in contexts
219+
* where there are existing v1 UUIDs. Systems that do not involve legacy UUIDv1
220+
* SHOULD consider using UUIDv7 instead.
195221
*
196222
* @return string The string standard representation of the UUID
197223
*/
198224
public static function uuid6(): string
199225
{
200-
[$unixts, $subsec] = self::getUnixTime();
226+
[$unixts, $subsec] = self::getUnixTimeSubsec();
201227
$timestamp = $unixts * self::SUBSEC_RANGE + $subsec;
202228
$timehex = str_pad(dechex($timestamp + self::TIME_OFFSET_INT), 15, '0', \STR_PAD_LEFT);
203229
$uhex = substr_replace(substr($timehex, -15), '6', -3, 0);
@@ -206,24 +232,43 @@ public static function uuid6(): string
206232
}
207233

208234
/**
209-
* Generate a version 7 UUID. A v7 UUID is lexicographically sortable and is
210-
* designed to encode a Unix timestamp with arbitrary sub-second precision.
235+
* UUID version 7 features a time-ordered value field derived from the widely
236+
* implemented and well known Unix Epoch timestamp source, the number of
237+
* milliseconds seconds since midnight 1 Jan 1970 UTC, leap seconds excluded. As
238+
* well as improved entropy characteristics over versions 1 or 6.
239+
*
240+
* Implementations SHOULD utilize UUID version 7 over UUID version 1 and 6 if
241+
* possible.
211242
*
212243
* @return string The string standard representation of the UUID
213244
*/
214245
public static function uuid7(): string
215246
{
216-
[$unixts, $subsec] = self::getUnixTime();
217-
$unixtsms = $unixts * 1000 + intdiv($subsec, self::V7_SUBSEC_RANGE);
218-
$subsec = self::encodeSubsec($subsec % self::V7_SUBSEC_RANGE);
247+
$unixtsms = self::getUnixTimeMs();
248+
$uhex = substr(str_pad(dechex($unixtsms), 12, '0', \STR_PAD_LEFT), -12);
249+
$uhex .= bin2hex(random_bytes(10));
250+
return self::uuidFromHex($uhex, 7);
251+
}
252+
253+
/**
254+
* Generate a version 8 UUID. A v8 UUID is lexicographically sortable and is
255+
* designed to encode a Unix timestamp with arbitrary sub-second precision.
256+
*
257+
* @return string The string standard representation of the UUID
258+
*/
259+
public static function uuid8(): string
260+
{
261+
[$unixts, $subsec] = self::getUnixTimeSubsec();
262+
$unixtsms = $unixts * 1000 + intdiv($subsec, self::V8_SUBSEC_RANGE);
263+
$subsec = self::encodeSubsec($subsec % self::V8_SUBSEC_RANGE);
219264
$subsecA = $subsec >> 2;
220265
$subsecB = $subsec & 0x03;
221266
$randB = random_bytes(8);
222267
$randB[0] = chr(ord($randB[0]) & 0x0f | $subsecB << 4);
223268
$uhex = substr(str_pad(dechex($unixtsms), 12, '0', \STR_PAD_LEFT), -12);
224-
$uhex .= '7' . str_pad(dechex($subsecA), 3, '0', \STR_PAD_LEFT);
269+
$uhex .= '8' . str_pad(dechex($subsecA), 3, '0', \STR_PAD_LEFT);
225270
$uhex .= bin2hex($randB);
226-
return self::uuidFromHex($uhex, 7);
271+
return self::uuidFromHex($uhex, 8);
227272
}
228273

229274
/**
@@ -270,9 +315,13 @@ public static function getTime(string $uuid): ?string
270315
}
271316
$retval .= substr_replace(str_pad(strval($ts), 8, '0', \STR_PAD_LEFT), '.', -7, 0);
272317
} elseif ($version === 7) {
318+
$unixts = hexdec(substr($timehex, 0, 13));
319+
$retval = strval($unixts * self::V7_SUBSEC_RANGE);
320+
$retval = substr_replace(str_pad($retval, 8, '0', \STR_PAD_LEFT), '.', -7, 0);
321+
} elseif ($version === 8) {
273322
$unixts = hexdec(substr($timehex, 0, 13));
274323
$subsec = self::decodeSubsec((hexdec(substr($timehex, 13)) << 2) + (hexdec(substr($uuid, 16, 1)) & 0x03));
275-
$retval = strval($unixts * self::V7_SUBSEC_RANGE + $subsec);
324+
$retval = strval($unixts * self::V8_SUBSEC_RANGE + $subsec);
276325
$retval = substr_replace(str_pad($retval, 8, '0', \STR_PAD_LEFT), '.', -7, 0);
277326
}
278327
return $retval;
@@ -354,4 +403,12 @@ public static function v7(): string
354403
{
355404
return self::uuid7();
356405
}
406+
/**
407+
* @see UUID::uuid8() Alias
408+
* @return string
409+
*/
410+
public static function v8(): string
411+
{
412+
return self::uuid8();
413+
}
357414
}

tests/Benchmark/UUIDGenerationBench.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,10 @@ public function benchUUID7Generation(): void
3131
{
3232
UUID::uuid7();
3333
}
34+
public function benchUUID8Generation(): void
35+
{
36+
UUID::uuid8();
37+
}
3438
public function benchUUIDToString(): void
3539
{
3640
UUID::toString('{C4A760A8-DBCF-5254-A0D9-6A4474BD1B62}');

tests/FutureTimeTest.php

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,9 @@ protected function setUp(): void
2222
$property = $reflection->getProperty('subsec');
2323
$property->setAccessible(true);
2424
$property->setValue($a, 9999990);
25+
$property = $reflection->getProperty('unixts_ms');
26+
$property->setAccessible(true);
27+
$property->setValue($a, 9000000000090);
2528
}
2629

2730
protected function tearDown(): void
@@ -34,6 +37,9 @@ protected function tearDown(): void
3437
$property = $reflection->getProperty('subsec');
3538
$property->setAccessible(true);
3639
$property->setValue($a, 0);
40+
$property = $reflection->getProperty('unixts_ms');
41+
$property->setAccessible(true);
42+
$property->setValue($a, 0);
3743
}
3844

3945
public function testFutureTimeVersion6()
@@ -69,4 +75,21 @@ public function testFutureTimeVersion7()
6975
$uuid1 = $uuid2;
7076
}
7177
}
78+
79+
public function testFutureTimeVersion8()
80+
{
81+
$uuid1 = UUID::uuid8();
82+
for ($x = 0; $x < 1000; $x++) {
83+
$uuid2 = UUID::uuid8();
84+
$this->assertGreaterThan(
85+
$uuid1,
86+
$uuid2
87+
);
88+
$this->assertLessThan(
89+
0,
90+
strcmp(UUID::getTime($uuid1), UUID::getTime($uuid2))
91+
);
92+
$uuid1 = $uuid2;
93+
}
94+
}
7295
}

0 commit comments

Comments
 (0)