14
14
use Symfony \Component \Uid \Exception \InvalidArgumentException ;
15
15
16
16
/**
17
- * A v7 UUID is lexicographically sortable and contains a 48 -bit timestamp and 74 extra unique bits.
17
+ * A v7 UUID is lexicographically sortable and contains a 58 -bit timestamp and 64 extra unique bits.
18
18
*
19
- * Within the same millisecond, monotonicity is ensured by incrementing the random part by a random increment.
19
+ * Within the same millisecond, the unique bits are incremented by a 24-bit random number.
20
+ * This method provides microsecond precision for the timestamp, and minimizes both the
21
+ * risk of collisions and the consumption of the OS' entropy pool.
20
22
*
21
23
* @author Nicolas Grekas <p@tchwork.com>
22
24
*/
@@ -25,6 +27,7 @@ class UuidV7 extends Uuid implements TimeBasedUidInterface
25
27
protected const TYPE = 7 ;
26
28
27
29
private static string $ time = '' ;
30
+ private static int $ subMs = 0 ;
28
31
private static array $ rand = [];
29
32
private static string $ seed ;
30
33
private static array $ seedParts ;
@@ -47,23 +50,27 @@ public function getDateTime(): \DateTimeImmutable
47
50
if (4 > \strlen ($ time )) {
48
51
$ time = '000 ' .$ time ;
49
52
}
53
+ $ time .= substr (1000 + (hexdec (substr ($ this ->uid , 14 , 4 )) >> 2 & 0x3FF ), -3 );
50
54
51
- return \DateTimeImmutable::createFromFormat ('U.v ' , substr_replace ($ time , '. ' , -3 , 0 ));
55
+ return \DateTimeImmutable::createFromFormat ('U.u ' , substr_replace ($ time , '. ' , -6 , 0 ));
52
56
}
53
57
54
58
public static function generate (?\DateTimeInterface $ time = null ): string
55
59
{
56
60
if (null === $ mtime = $ time ) {
57
61
$ time = microtime (false );
62
+ $ subMs = (int ) substr ($ time , 5 , 3 );
58
63
$ time = substr ($ time , 11 ).substr ($ time , 2 , 3 );
59
- } elseif (0 > $ time = $ time ->format ('Uv ' )) {
64
+ } elseif (0 > $ time = $ time ->format ('Uu ' )) {
60
65
throw new InvalidArgumentException ('The timestamp must be positive. ' );
66
+ } else {
67
+ $ subMs = (int ) substr ($ time , -3 );
68
+ $ time = substr ($ time , 0 , -3 );
61
69
}
62
70
63
71
if ($ time > self ::$ time || (null !== $ mtime && $ time !== self ::$ time )) {
64
72
randomize:
65
- self ::$ rand = unpack ('n* ' , isset (self ::$ seed ) ? random_bytes (10 ) : self ::$ seed = random_bytes (16 ));
66
- self ::$ rand [1 ] &= 0x03FF ;
73
+ self ::$ rand = unpack (\PHP_INT_SIZE >= 8 ? 'L* ' : 'n* ' , isset (self ::$ seed ) ? random_bytes (8 ) : self ::$ seed = random_bytes (16 ));
67
74
self ::$ time = $ time ;
68
75
} else {
69
76
// Within the same ms, we increment the rand part by a random 24-bit number.
@@ -73,8 +80,8 @@ public static function generate(?\DateTimeInterface $time = null): string
73
80
// them into 16 x 32-bit numbers; we take the first byte of each of these
74
81
// numbers to get 5 extra 24-bit numbers. Then, we consume those numbers
75
82
// one-by-one and run this logic every 21 iterations.
76
- // self::$rand holds the random part of the UUID, split into 5 x 16 -bit
77
- // numbers for x86 portability. We increment this random part by the next
83
+ // self::$rand holds the random part of the UUID, split into 2 x 32 -bit numbers
84
+ // or 4 x 16-bit for x86 portability. We increment this random part by the next
78
85
// 24-bit number in the self::$seedParts list and decrement self::$seedIndex.
79
86
80
87
if (!self ::$ seedIndex ) {
@@ -88,40 +95,55 @@ public static function generate(?\DateTimeInterface $time = null): string
88
95
self ::$ seedIndex = 21 ;
89
96
}
90
97
91
- self :: $ rand [ 5 ] = 0xFFFF & $ carry = self :: $ rand [ 5 ] + 1 + ( self :: $ seedParts [ self :: $ seedIndex --] & 0xFFFFFF );
92
- self ::$ rand [4 ] = 0xFFFF & $ carry = self ::$ rand [4 ] + ( $ carry >> 16 );
93
- self ::$ rand [3 ] = 0xFFFF & $ carry = self ::$ rand [3 ] + ($ carry >> 16 );
94
- self :: $ rand [ 2 ] = 0xFFFF & $ carry = self :: $ rand [ 2 ] + ( $ carry >> 16 ) ;
95
- self :: $ rand [ 1 ] += $ carry >> 16 ;
96
-
97
- if ( 0xFC00 & self ::$ rand [1 ]) {
98
- if (\ PHP_INT_SIZE >= 8 || 10 > \strlen ( $ time = self ::$ time )) {
99
- $ time = ( string ) ( 1 + $ time );
100
- } elseif ( ' 999999999 ' === $ mtime = substr ( $ time , - 9 )) {
101
- $ time = ( 1 + substr ( $ time , 0 , - 9 )). ' 000000000 ' ;
102
- } else {
103
- $ time = substr_replace ( $ time , str_pad (++ $ mtime , 9 , ' 0 ' , \ STR_PAD_LEFT ), - 9 );
104
- }
98
+ if (\ PHP_INT_SIZE >= 8 ) {
99
+ self ::$ rand [2 ] = 0xFFFFFFFF & $ carry = self ::$ rand [2 ] + 1 + ( self :: $ seedParts [ self :: $ seedIndex --] & 0xFFFFFF );
100
+ self ::$ rand [1 ] = 0xFFFFFFFF & $ carry = self ::$ rand [1 ] + ($ carry >> 32 );
101
+ $ carry >>= 32 ;
102
+ } else {
103
+ self :: $ rand [ 4 ] = 0xFFFF & $ carry = self :: $ rand [ 4 ] + 1 + ( self :: $ seedParts [ self :: $ seedIndex --] & 0xFFFFFF );
104
+ self :: $ rand [ 3 ] = 0xFFFF & $ carry = self ::$ rand [3 ] + ( $ carry >> 16 );
105
+ self :: $ rand [ 2 ] = 0xFFFF & $ carry = self ::$ rand [ 2 ] + ( $ carry >> 16 );
106
+ self :: $ rand [ 1 ] = 0xFFFF & $ carry = self :: $ rand [ 1 ] + ( $ carry >> 16 );
107
+ $ carry >>= 16 ;
108
+ }
109
+
110
+ if ( $ carry && $ subMs <= self :: $ subMs ) {
111
+ usleep ( 1 );
105
112
106
- goto randomize;
113
+ if (1024 <= ++$ subMs ) {
114
+ if (\PHP_INT_SIZE >= 8 || 10 > \strlen ($ time = self ::$ time )) {
115
+ $ time = (string ) (1 + $ time );
116
+ } elseif ('999999999 ' === $ mtime = substr ($ time , -9 )) {
117
+ $ time = (1 + substr ($ time , 0 , -9 )).'000000000 ' ;
118
+ } else {
119
+ $ time = substr_replace ($ time , str_pad (++$ mtime , 9 , '0 ' , \STR_PAD_LEFT ), -9 );
120
+ }
121
+
122
+ goto randomize;
123
+ }
107
124
}
108
125
109
126
$ time = self ::$ time ;
110
127
}
128
+ self ::$ subMs = $ subMs ;
111
129
112
130
if (\PHP_INT_SIZE >= 8 ) {
113
- $ time = dechex ($ time );
114
- } else {
115
- $ time = bin2hex (BinaryUtil::fromBase ($ time , BinaryUtil::BASE10 ));
131
+ return substr_replace (\sprintf ('%012x-%04x-%04x-%04x%08x ' ,
132
+ $ time ,
133
+ 0x7000 | ($ subMs << 2 ) | (self ::$ rand [1 ] >> 30 ),
134
+ 0x8000 | (self ::$ rand [1 ] >> 16 & 0x3FFF ),
135
+ self ::$ rand [1 ] & 0xFFFF ,
136
+ self ::$ rand [2 ],
137
+ ), '- ' , 8 , 0 );
116
138
}
117
139
118
140
return substr_replace (\sprintf ('%012s-%04x-%04x-%04x%04x%04x ' ,
119
- $ time ,
120
- 0x7000 | (self ::$ rand [1 ] << 2 ) | (self ::$ rand [2 ] >> 14 ),
121
- 0x8000 | (self ::$ rand [2 ] & 0x3FFF ),
141
+ bin2hex (BinaryUtil::fromBase ($ time , BinaryUtil::BASE10 )),
142
+ 0x7000 | ($ subMs << 2 ) | (self ::$ rand [1 ] >> 14 ),
143
+ 0x8000 | (self ::$ rand [1 ] & 0x3FFF ),
144
+ self ::$ rand [2 ],
122
145
self ::$ rand [3 ],
123
146
self ::$ rand [4 ],
124
- self ::$ rand [5 ],
125
147
), '- ' , 8 , 0 );
126
148
}
127
149
}
0 commit comments