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
}

src/Symfony/Component/Uid/UuidV7.php

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)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy