1414use Symfony \Component \Uid \Exception \InvalidArgumentException ;
1515
1616/**
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.
1818 *
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.
2022 *
2123 * @author Nicolas Grekas <p@tchwork.com>
2224 */
@@ -25,6 +27,7 @@ class UuidV7 extends Uuid implements TimeBasedUidInterface
2527 protected const TYPE = 7 ;
2628
2729 private static string $ time = '' ;
30+ private static int $ subMs = 0 ;
2831 private static array $ rand = [];
2932 private static string $ seed ;
3033 private static array $ seedParts ;
@@ -47,23 +50,27 @@ public function getDateTime(): \DateTimeImmutable
4750 if (4 > \strlen ($ time )) {
4851 $ time = '000 ' .$ time ;
4952 }
53+ $ time .= substr (1000 + (hexdec (substr ($ this ->uid , 14 , 4 )) >> 2 & 0x3FF ), -3 );
5054
51- return \DateTimeImmutable::createFromFormat ('U.v ' , substr_replace ($ time , '. ' , -3 , 0 ));
55+ return \DateTimeImmutable::createFromFormat ('U.u ' , substr_replace ($ time , '. ' , -6 , 0 ));
5256 }
5357
5458 public static function generate (?\DateTimeInterface $ time = null ): string
5559 {
5660 if (null === $ mtime = $ time ) {
5761 $ time = microtime (false );
62+ $ subMs = (int ) substr ($ time , 5 , 3 );
5863 $ time = substr ($ time , 11 ).substr ($ time , 2 , 3 );
59- } elseif (0 > $ time = $ time ->format ('Uv ' )) {
64+ } elseif (0 > $ time = $ time ->format ('Uu ' )) {
6065 throw new InvalidArgumentException ('The timestamp must be positive. ' );
66+ } else {
67+ $ subMs = (int ) substr ($ time , -3 );
68+ $ time = substr ($ time , 0 , -3 );
6169 }
6270
6371 if ($ time > self ::$ time || (null !== $ mtime && $ time !== self ::$ time )) {
6472 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 ));
6774 self ::$ time = $ time ;
6875 } else {
6976 // 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
7380 // them into 16 x 32-bit numbers; we take the first byte of each of these
7481 // numbers to get 5 extra 24-bit numbers. Then, we consume those numbers
7582 // 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
7885 // 24-bit number in the self::$seedParts list and decrement self::$seedIndex.
7986
8087 if (!self ::$ seedIndex ) {
@@ -88,40 +95,55 @@ public static function generate(?\DateTimeInterface $time = null): string
8895 self ::$ seedIndex = 21 ;
8996 }
9097
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 );
105112
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+ }
107124 }
108125
109126 $ time = self ::$ time ;
110127 }
128+ self ::$ subMs = $ subMs ;
111129
112130 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 );
116138 }
117139
118140 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 ],
122145 self ::$ rand [3 ],
123146 self ::$ rand [4 ],
124- self ::$ rand [5 ],
125147 ), '- ' , 8 , 0 );
126148 }
127149}
0 commit comments