Skip to content

Commit 6bde414

Browse files
[Uid] Add UuidV7 and UuidV8
1 parent c991df6 commit 6bde414

File tree

13 files changed

+243
-14
lines changed

13 files changed

+243
-14
lines changed

src/Symfony/Component/Form/Extension/Core/DataTransformer/UuidToStringTransformer.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ public function reverseTransform(mixed $value): ?Uuid
6161
}
6262

6363
try {
64-
$uuid = new Uuid($value);
64+
$uuid = Uuid::fromString($value);
6565
} catch (\InvalidArgumentException $e) {
6666
throw new TransformationFailedException(sprintf('The value "%s" is not a valid UUID.', $value), $e->getCode(), $e);
6767
}

src/Symfony/Component/Routing/Requirement/Requirement.php

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,10 +25,12 @@ enum Requirement
2525
public const UID_BASE58 = '[1-9A-HJ-NP-Za-km-z]{22}';
2626
public const UID_RFC4122 = '[0-9a-f]{8}(?:-[0-9a-f]{4}){3}-[0-9a-f]{12}';
2727
public const ULID = '[0-7][0-9A-HJKMNP-TV-Z]{25}';
28-
public const UUID = '[0-9a-f]{8}-[0-9a-f]{4}-[1-6][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}';
28+
public const UUID = '[0-9a-f]{8}-[0-9a-f]{4}-[13-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}';
2929
public const UUID_V1 = '[0-9a-f]{8}-[0-9a-f]{4}-1[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}';
3030
public const UUID_V3 = '[0-9a-f]{8}-[0-9a-f]{4}-3[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}';
3131
public const UUID_V4 = '[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}';
3232
public const UUID_V5 = '[0-9a-f]{8}-[0-9a-f]{4}-5[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}';
3333
public const UUID_V6 = '[0-9a-f]{8}-[0-9a-f]{4}-6[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}';
34+
public const UUID_V7 = '[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}';
35+
public const UUID_V8 = '[0-9a-f]{8}-[0-9a-f]{4}-8[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}';
3436
}

src/Symfony/Component/Uid/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ CHANGELOG
44
6.2
55
---
66

7+
* Add `UuidV7` and `UuidV8`
78
* Add `TimeBasedUidInterface` to describe UIDs that embed a timestamp
89
* Add `MaxUuid` and `MaxUlid`
910

src/Symfony/Component/Uid/Factory/UuidFactory.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ public function __construct(string|int $defaultClass = UuidV6::class, string|int
4444
$this->nameBasedNamespace = $nameBasedNamespace;
4545
}
4646

47-
public function create(): UuidV6|UuidV4|UuidV1
47+
public function create(): Uuid
4848
{
4949
$class = $this->defaultClass;
5050

src/Symfony/Component/Uid/README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,9 @@ Uid Component
33

44
The UID component provides an object-oriented API to generate and represent UIDs.
55

6+
It provides implementations for UUIDs version 1 and versions 3 to 8,
7+
for ULIDs and for related factories.
8+
69
Resources
710
---------
811

src/Symfony/Component/Uid/Tests/Command/InspectUuidCommandTest.php

Lines changed: 50 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -82,16 +82,16 @@ public function testUnknown()
8282
EOF
8383
, $commandTester->getDisplay(true));
8484

85-
$this->assertSame(0, $commandTester->execute(['uuid' => '461cc9b9-2397-7dba-91e9-33af4c63f7ec']));
85+
$this->assertSame(0, $commandTester->execute(['uuid' => '461cc9b9-2397-adba-91e9-33af4c63f7ec']));
8686
$this->assertSame(<<<EOF
8787
----------------------- --------------------------------------
8888
Label Value
8989
----------------------- --------------------------------------
90-
Version 7
91-
toRfc4122 (canonical) 461cc9b9-2397-7dba-91e9-33af4c63f7ec
92-
toBase58 9f9nftX6kE2K6HpooNEQ83
93-
toBase32 263K4VJ8WQFPX93T9KNX667XZC
94-
toHex 0x461cc9b923977dba91e933af4c63f7ec
90+
Version 10
91+
toRfc4122 (canonical) 461cc9b9-2397-adba-91e9-33af4c63f7ec
92+
toBase58 9f9nftX6nvS6vPZqBckwvj
93+
toBase32 263K4VJ8WQNPX93T9KNX667XZC
94+
toHex 0x461cc9b92397adba91e933af4c63f7ec
9595
----------------------- --------------------------------------
9696
9797
@@ -220,6 +220,50 @@ public function testV6()
220220
----------------------- --------------------------------------
221221
222222
223+
EOF
224+
, $commandTester->getDisplay(true));
225+
}
226+
227+
public function testV7()
228+
{
229+
$commandTester = new CommandTester(new InspectUuidCommand());
230+
231+
$this->assertSame(0, $commandTester->execute(['uuid' => '017f22e2-79b0-7cc3-98c4-dc0c0c07398f']));
232+
$this->assertSame(<<<EOF
233+
----------------------- --------------------------------------
234+
Label Value
235+
----------------------- --------------------------------------
236+
Version 7
237+
toRfc4122 (canonical) 017f22e2-79b0-7cc3-98c4-dc0c0c07398f
238+
toBase58 1BihbxwwQ4NZZpKRH9JDCz
239+
toBase32 01FWHE4YDGFK1SHH6W1G60EECF
240+
toHex 0x017f22e279b07cc398c4dc0c0c07398f
241+
----------------------- --------------------------------------
242+
Time 2022-02-22 19:22:22.000000 UTC
243+
----------------------- --------------------------------------
244+
245+
246+
EOF
247+
, $commandTester->getDisplay(true));
248+
}
249+
250+
public function testV8()
251+
{
252+
$commandTester = new CommandTester(new InspectUuidCommand());
253+
254+
$this->assertSame(0, $commandTester->execute(['uuid' => '017f22e2-79b0-8cc3-98c4-dc0c0c07398f']));
255+
$this->assertSame(<<<EOF
256+
----------------------- --------------------------------------
257+
Label Value
258+
----------------------- --------------------------------------
259+
Version 8
260+
toRfc4122 (canonical) 017f22e2-79b0-8cc3-98c4-dc0c0c07398f
261+
toBase58 1BihbxwwQxWVWWu6QZUPot
262+
toBase32 01FWHE4YDGHK1SHH6W1G60EECF
263+
toHex 0x017f22e279b08cc398c4dc0c0c07398f
264+
----------------------- --------------------------------------
265+
266+
223267
EOF
224268
, $commandTester->getDisplay(true));
225269
}

src/Symfony/Component/Uid/Tests/UuidTest.php

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,11 +22,13 @@
2222
use Symfony\Component\Uid\UuidV4;
2323
use Symfony\Component\Uid\UuidV5;
2424
use Symfony\Component\Uid\UuidV6;
25+
use Symfony\Component\Uid\UuidV7;
2526

2627
class UuidTest extends TestCase
2728
{
2829
private const A_UUID_V1 = 'd9e7a184-5d5b-11ea-a62a-3499710062d0';
2930
private const A_UUID_V4 = 'd6b3345b-2905-4048-a83c-b5988e765d98';
31+
private const A_UUID_V7 = '017f22e2-79b0-7cc3-98c4-dc0c0c07398f';
3032

3133
/**
3234
* @dataProvider provideInvalidUuids
@@ -69,6 +71,8 @@ public function provideInvalidVariant(): iterable
6971
yield ['8dac64d3-937a-4e7c-fa1d-d5d6c06a61f5'];
7072
yield ['8dac64d3-937a-5e7c-fa1d-d5d6c06a61f5'];
7173
yield ['8dac64d3-937a-6e7c-fa1d-d5d6c06a61f5'];
74+
yield ['8dac64d3-937a-7e7c-fa1d-d5d6c06a61f5'];
75+
yield ['8dac64d3-937a-8e7c-fa1d-d5d6c06a61f5'];
7276
}
7377

7478
public function testConstructorWithValidUuid()
@@ -134,6 +138,28 @@ public function testV6IsSeeded()
134138
$this->assertNotSame(substr($uuidV1, 24), substr($uuidV6, 24));
135139
}
136140

141+
public function testV7()
142+
{
143+
$uuid = Uuid::fromString(self::A_UUID_V7);
144+
145+
$this->assertInstanceOf(UuidV7::class, $uuid);
146+
$this->assertSame(1645557742, $uuid->getDateTime()->getTimeStamp());
147+
148+
$prev = UuidV7::generate();
149+
150+
for ($i = 0; $i < 25; ++$i) {
151+
$uuid = UuidV7::generate();
152+
$now = gmdate('Y-m-d H:i');
153+
$this->assertGreaterThan($prev, $uuid);
154+
$prev = $uuid;
155+
}
156+
157+
$this->assertTrue(Uuid::isValid($uuid));
158+
$uuid = Uuid::fromString($uuid);
159+
$this->assertInstanceOf(UuidV7::class, $uuid);
160+
$this->assertSame($now, $uuid->getDateTime()->format('Y-m-d H:i'));
161+
}
162+
137163
public function testBinary()
138164
{
139165
$uuid = new UuidV4(self::A_UUID_V4);

src/Symfony/Component/Uid/Uuid.php

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,8 @@ public static function fromString(string $uuid): static
8383
UuidV4::TYPE => new UuidV4($uuid),
8484
UuidV5::TYPE => new UuidV5($uuid),
8585
UuidV6::TYPE => new UuidV6($uuid),
86+
UuidV7::TYPE => new UuidV7($uuid),
87+
UuidV8::TYPE => new UuidV8($uuid),
8688
default => new self($uuid),
8789
};
8890
}
@@ -118,6 +120,16 @@ final public static function v6(): UuidV6
118120
return new UuidV6();
119121
}
120122

123+
final public static function v7(): UuidV7
124+
{
125+
return new UuidV7();
126+
}
127+
128+
final public static function v8(string $uuid): UuidV8
129+
{
130+
return new UuidV8($uuid);
131+
}
132+
121133
public static function isValid(string $uuid): bool
122134
{
123135
if (!preg_match('{^[0-9a-f]{8}(?:-[0-9a-f]{4}){2}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$}Di', $uuid)) {

src/Symfony/Component/Uid/UuidV1.php

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ class UuidV1 extends Uuid implements TimeBasedUidInterface
2020
{
2121
protected const TYPE = 1;
2222

23-
private static ?string $clockSeq = null;
23+
private static string $clockSeq;
2424

2525
public function __construct(string $uuid = null)
2626
{
@@ -49,13 +49,13 @@ public static function generate(\DateTimeInterface $time = null, Uuid $node = nu
4949
if ($node) {
5050
// use clock_seq from the node
5151
$seq = substr($node->uid, 19, 4);
52-
} else {
52+
} elseif (!$seq = self::$clockSeq ?? '') {
5353
// generate a static random clock_seq to prevent any collisions with the real one
5454
$seq = substr($uuid, 19, 4);
5555

56-
while (null === self::$clockSeq || $seq === self::$clockSeq) {
56+
do {
5757
self::$clockSeq = sprintf('%04x', random_int(0, 0x3FFF) | 0x8000);
58-
}
58+
} while ($seq === self::$clockSeq);
5959

6060
$seq = self::$clockSeq;
6161
}
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\Uid;
13+
14+
/**
15+
* A v7 UUID is lexicographically sortable and contains a 48-bit timestamp and 74 extra unique bits.
16+
*
17+
* Within the same millisecond, monotonicity is ensured by incrementing the random part by a random increment.
18+
*
19+
* @author Nicolas Grekas <p@tchwork.com>
20+
*/
21+
class UuidV7 extends Uuid implements TimeBasedUidInterface
22+
{
23+
protected const TYPE = 7;
24+
25+
private static string $time = '';
26+
private static array $rand = [];
27+
private static string $seed;
28+
private static array $seedParts;
29+
private static int $seedIndex = 0;
30+
31+
public function __construct(string $uuid = null)
32+
{
33+
if (null === $uuid) {
34+
$this->uid = static::generate();
35+
} else {
36+
parent::__construct($uuid, true);
37+
}
38+
}
39+
40+
public function getDateTime(): \DateTimeImmutable
41+
{
42+
$time = substr($this->uid, 0, 8).substr($this->uid, 9, 4);
43+
$time = \PHP_INT_SIZE >= 8 ? (string) hexdec($time) : BinaryUtil::toBase(hex2bin($time), BinaryUtil::BASE10);
44+
45+
if (4 > \strlen($time)) {
46+
$time = '000'.$time;
47+
}
48+
49+
return \DateTimeImmutable::createFromFormat('U.v', substr_replace($time, '.', -3, 0));
50+
}
51+
52+
public static function generate(\DateTimeInterface $time = null): string
53+
{
54+
if (null === $mtime = $time) {
55+
$time = microtime(false);
56+
$time = substr($time, 11).substr($time, 2, 3);
57+
} elseif (0 > $time = $time->format('Uv')) {
58+
throw new \InvalidArgumentException('The timestamp must be positive.');
59+
}
60+
61+
if ($time > self::$time || (null !== $mtime && $time !== self::$time)) {
62+
randomize:
63+
$s = isset(self::$seed) ? random_bytes(10) : self::$seed = random_bytes(16);
64+
self::$rand = array_values(unpack('nr1/nr2/nr3/nr4/nr5', $s));
65+
self::$rand[0] &= 0x03FF;
66+
self::$time = $time;
67+
} else {
68+
if (!self::$seedIndex) {
69+
self::$seedParts = unpack('l*', self::$seed = md5(self::$seed, true));
70+
self::$seedIndex = 4;
71+
}
72+
$carry = self::$seedParts[self::$seedIndex--] & 0xFFFFFF;
73+
74+
for ($i = 4; 0 <= $i; --$i) {
75+
$carry += self::$rand[$i];
76+
self::$rand[$i] = $carry & 0xFFFF;
77+
$carry >>= 16;
78+
}
79+
80+
if (0xFC00 & self::$rand[0]) {
81+
if (\PHP_INT_SIZE >= 8 || 10 > \strlen($time)) {
82+
$time = (string) (1 + $time);
83+
} elseif ('999999999' === $mtime = substr($time, -9)) {
84+
$time = (1 + substr($time, 0, -9)).'000000000';
85+
} else {
86+
$time = substr_replace($time, str_pad(++$mtime, 9, '0', \STR_PAD_LEFT), -9);
87+
}
88+
89+
goto randomize;
90+
}
91+
92+
$time = self::$time;
93+
}
94+
95+
if (\PHP_INT_SIZE >= 8) {
96+
$time = base_convert($time, 10, 16);
97+
} else {
98+
$time = bin2hex(BinaryUtil::fromBase($time, BinaryUtil::BASE10));
99+
}
100+
101+
return substr_replace(sprintf('%012s-%04x-%04x-%04x%04x%04x',
102+
$time,
103+
0x7000 | (self::$rand[0] << 2) | (self::$rand[1] >> 14),
104+
0x8000 | (self::$rand[1] & 0x3FFF),
105+
self::$rand[2],
106+
self::$rand[3],
107+
self::$rand[4],
108+
), '-', 8, 0);
109+
}
110+
}

0 commit comments

Comments
 (0)