Thanks to visit codestin.com
Credit goes to github.com

Skip to content

[Uid] Add UuidV7 and UuidV8 #47525

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Sep 9, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion src/Symfony/Component/Routing/Requirement/Requirement.php
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,12 @@ enum Requirement
public const UID_BASE58 = '[1-9A-HJ-NP-Za-km-z]{22}';
public const UID_RFC4122 = '[0-9a-f]{8}(?:-[0-9a-f]{4}){3}-[0-9a-f]{12}';
public const ULID = '[0-7][0-9A-HJKMNP-TV-Z]{25}';
public const UUID = '[0-9a-f]{8}-[0-9a-f]{4}-[13-6][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}';
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}';
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}';
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}';
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}';
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}';
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}';
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}';
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}';
}
1 change: 1 addition & 0 deletions src/Symfony/Component/Uid/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ CHANGELOG
6.2
---

* Add `UuidV7` and `UuidV8`
* Add `TimeBasedUidInterface` to describe UIDs that embed a timestamp
* Add `MaxUuid` and `MaxUlid`

Expand Down
2 changes: 1 addition & 1 deletion src/Symfony/Component/Uid/Factory/UuidFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ public function __construct(string|int $defaultClass = UuidV6::class, string|int
$this->nameBasedNamespace = $nameBasedNamespace;
}

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

Expand Down
3 changes: 3 additions & 0 deletions src/Symfony/Component/Uid/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@ Uid Component

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

It provides implementations for UUIDs version 1 and versions 3 to 8,
for ULIDs and for related factories.

Resources
---------

Expand Down
56 changes: 50 additions & 6 deletions src/Symfony/Component/Uid/Tests/Command/InspectUuidCommandTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -82,16 +82,16 @@ public function testUnknown()
EOF
, $commandTester->getDisplay(true));

$this->assertSame(0, $commandTester->execute(['uuid' => '461cc9b9-2397-7dba-91e9-33af4c63f7ec']));
$this->assertSame(0, $commandTester->execute(['uuid' => '461cc9b9-2397-adba-91e9-33af4c63f7ec']));
$this->assertSame(<<<EOF
----------------------- --------------------------------------
Label Value
----------------------- --------------------------------------
Version 7
toRfc4122 (canonical) 461cc9b9-2397-7dba-91e9-33af4c63f7ec
toBase58 9f9nftX6kE2K6HpooNEQ83
toBase32 263K4VJ8WQFPX93T9KNX667XZC
toHex 0x461cc9b923977dba91e933af4c63f7ec
Version 10
toRfc4122 (canonical) 461cc9b9-2397-adba-91e9-33af4c63f7ec
toBase58 9f9nftX6nvS6vPZqBckwvj
toBase32 263K4VJ8WQNPX93T9KNX667XZC
toHex 0x461cc9b92397adba91e933af4c63f7ec
----------------------- --------------------------------------


Expand Down Expand Up @@ -220,6 +220,50 @@ public function testV6()
----------------------- --------------------------------------


EOF
, $commandTester->getDisplay(true));
}

public function testV7()
{
$commandTester = new CommandTester(new InspectUuidCommand());

$this->assertSame(0, $commandTester->execute(['uuid' => '017f22e2-79b0-7cc3-98c4-dc0c0c07398f']));
$this->assertSame(<<<EOF
----------------------- --------------------------------------
Label Value
----------------------- --------------------------------------
Version 7
toRfc4122 (canonical) 017f22e2-79b0-7cc3-98c4-dc0c0c07398f
toBase58 1BihbxwwQ4NZZpKRH9JDCz
toBase32 01FWHE4YDGFK1SHH6W1G60EECF
toHex 0x017f22e279b07cc398c4dc0c0c07398f
----------------------- --------------------------------------
Time 2022-02-22 19:22:22.000000 UTC
----------------------- --------------------------------------


EOF
, $commandTester->getDisplay(true));
}

public function testV8()
{
$commandTester = new CommandTester(new InspectUuidCommand());

$this->assertSame(0, $commandTester->execute(['uuid' => '017f22e2-79b0-8cc3-98c4-dc0c0c07398f']));
$this->assertSame(<<<EOF
----------------------- --------------------------------------
Label Value
----------------------- --------------------------------------
Version 8
toRfc4122 (canonical) 017f22e2-79b0-8cc3-98c4-dc0c0c07398f
toBase58 1BihbxwwQxWVWWu6QZUPot
toBase32 01FWHE4YDGHK1SHH6W1G60EECF
toHex 0x017f22e279b08cc398c4dc0c0c07398f
----------------------- --------------------------------------


EOF
, $commandTester->getDisplay(true));
}
Expand Down
26 changes: 26 additions & 0 deletions src/Symfony/Component/Uid/Tests/UuidTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,13 @@
use Symfony\Component\Uid\UuidV4;
use Symfony\Component\Uid\UuidV5;
use Symfony\Component\Uid\UuidV6;
use Symfony\Component\Uid\UuidV7;

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

/**
* @dataProvider provideInvalidUuids
Expand Down Expand Up @@ -69,6 +71,8 @@ public function provideInvalidVariant(): iterable
yield ['8dac64d3-937a-4e7c-fa1d-d5d6c06a61f5'];
yield ['8dac64d3-937a-5e7c-fa1d-d5d6c06a61f5'];
yield ['8dac64d3-937a-6e7c-fa1d-d5d6c06a61f5'];
yield ['8dac64d3-937a-7e7c-fa1d-d5d6c06a61f5'];
yield ['8dac64d3-937a-8e7c-fa1d-d5d6c06a61f5'];
}

public function testConstructorWithValidUuid()
Expand Down Expand Up @@ -134,6 +138,28 @@ public function testV6IsSeeded()
$this->assertNotSame(substr($uuidV1, 24), substr($uuidV6, 24));
}

public function testV7()
{
$uuid = Uuid::fromString(self::A_UUID_V7);

$this->assertInstanceOf(UuidV7::class, $uuid);
$this->assertSame(1645557742, $uuid->getDateTime()->getTimeStamp());

$prev = UuidV7::generate();

for ($i = 0; $i < 25; ++$i) {
$uuid = UuidV7::generate();
$now = gmdate('Y-m-d H:i');
$this->assertGreaterThan($prev, $uuid);
$prev = $uuid;
}

$this->assertTrue(Uuid::isValid($uuid));
$uuid = Uuid::fromString($uuid);
$this->assertInstanceOf(UuidV7::class, $uuid);
$this->assertSame($now, $uuid->getDateTime()->format('Y-m-d H:i'));
}

public function testBinary()
{
$uuid = new UuidV4(self::A_UUID_V4);
Expand Down
22 changes: 11 additions & 11 deletions src/Symfony/Component/Uid/Ulid.php
Original file line number Diff line number Diff line change
Expand Up @@ -152,15 +152,15 @@ public static function generate(\DateTimeInterface $time = null): string

if ($time > self::$time || (null !== $mtime && $time !== self::$time)) {
randomize:
$r = unpack('nr1/nr2/nr3/nr4/nr', random_bytes(10));
$r['r1'] |= ($r['r'] <<= 4) & 0xF0000;
$r['r2'] |= ($r['r'] <<= 4) & 0xF0000;
$r['r3'] |= ($r['r'] <<= 4) & 0xF0000;
$r['r4'] |= ($r['r'] <<= 4) & 0xF0000;
unset($r['r']);
self::$rand = array_values($r);
$r = unpack('n*', random_bytes(10));
$r[1] |= ($r[5] <<= 4) & 0xF0000;
$r[2] |= ($r[5] <<= 4) & 0xF0000;
$r[3] |= ($r[5] <<= 4) & 0xF0000;
$r[4] |= ($r[5] <<= 4) & 0xF0000;
unset($r[5]);
self::$rand = $r;
self::$time = $time;
} elseif ([0xFFFFF, 0xFFFFF, 0xFFFFF, 0xFFFFF] === self::$rand) {
} elseif ([1 => 0xFFFFF, 0xFFFFF, 0xFFFFF, 0xFFFFF] === self::$rand) {
if (\PHP_INT_SIZE >= 8 || 10 > \strlen($time = self::$time)) {
$time = (string) (1 + $time);
} elseif ('999999999' === $mtime = substr($time, -9)) {
Expand All @@ -171,7 +171,7 @@ public static function generate(\DateTimeInterface $time = null): string

goto randomize;
} else {
for ($i = 3; $i >= 0 && 0xFFFFF === self::$rand[$i]; --$i) {
for ($i = 4; $i > 0 && 0xFFFFF === self::$rand[$i]; --$i) {
self::$rand[$i] = 0;
}

Expand All @@ -192,10 +192,10 @@ public static function generate(\DateTimeInterface $time = null): string

return strtr(sprintf('%010s%04s%04s%04s%04s',
$time,
base_convert(self::$rand[0], 10, 32),
base_convert(self::$rand[1], 10, 32),
base_convert(self::$rand[2], 10, 32),
base_convert(self::$rand[3], 10, 32)
base_convert(self::$rand[3], 10, 32),
base_convert(self::$rand[4], 10, 32)
), 'abcdefghijklmnopqrstuv', 'ABCDEFGHJKMNPQRSTVWXYZ');
}
}
12 changes: 12 additions & 0 deletions src/Symfony/Component/Uid/Uuid.php
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,8 @@ public static function fromString(string $uuid): static
UuidV4::TYPE => new UuidV4($uuid),
UuidV5::TYPE => new UuidV5($uuid),
UuidV6::TYPE => new UuidV6($uuid),
UuidV7::TYPE => new UuidV7($uuid),
UuidV8::TYPE => new UuidV8($uuid),
default => new self($uuid),
};
}
Expand Down Expand Up @@ -118,6 +120,16 @@ final public static function v6(): UuidV6
return new UuidV6();
}

final public static function v7(): UuidV7
{
return new UuidV7();
}

final public static function v8(string $uuid): UuidV8
{
return new UuidV8($uuid);
}

public static function isValid(string $uuid): bool
{
if (!preg_match('{^[0-9a-f]{8}(?:-[0-9a-f]{4}){2}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$}Di', $uuid)) {
Expand Down
8 changes: 4 additions & 4 deletions src/Symfony/Component/Uid/UuidV1.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ class UuidV1 extends Uuid implements TimeBasedUidInterface
{
protected const TYPE = 1;

private static ?string $clockSeq = null;
private static string $clockSeq;

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

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

$seq = self::$clockSeq;
}
Expand Down
114 changes: 114 additions & 0 deletions src/Symfony/Component/Uid/UuidV7.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
<?php

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <[email protected]>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Symfony\Component\Uid;

/**
* A v7 UUID is lexicographically sortable and contains a 48-bit timestamp and 74 extra unique bits.
*
* Within the same millisecond, monotonicity is ensured by incrementing the random part by a random increment.
*
* @author Nicolas Grekas <[email protected]>
*/
class UuidV7 extends Uuid implements TimeBasedUidInterface
{
protected const TYPE = 7;

private static string $time = '';
private static array $rand = [];
private static string $seed;
private static array $seedParts;
private static int $seedIndex = 0;

public function __construct(string $uuid = null)
{
if (null === $uuid) {
$this->uid = static::generate();
} else {
parent::__construct($uuid, true);
}
}

public function getDateTime(): \DateTimeImmutable
{
$time = substr($this->uid, 0, 8).substr($this->uid, 9, 4);
$time = \PHP_INT_SIZE >= 8 ? (string) hexdec($time) : BinaryUtil::toBase(hex2bin($time), BinaryUtil::BASE10);

if (4 > \strlen($time)) {
$time = '000'.$time;
}

return \DateTimeImmutable::createFromFormat('U.v', substr_replace($time, '.', -3, 0));
}

public static function generate(\DateTimeInterface $time = null): string
{
if (null === $mtime = $time) {
$time = microtime(false);
$time = substr($time, 11).substr($time, 2, 3);
} elseif (0 > $time = $time->format('Uv')) {
throw new \InvalidArgumentException('The timestamp must be positive.');
}

if ($time > self::$time || (null !== $mtime && $time !== self::$time)) {
randomize:
self::$rand = unpack('n*', isset(self::$seed) ? random_bytes(10) : self::$seed = random_bytes(16));
self::$rand[1] &= 0x03FF;
self::$time = $time;
} else {
if (!self::$seedIndex) {
$s = unpack('l*', self::$seed = hash('sha512', self::$seed, true));
$s[] = ($s[1] >> 8 & 0xFF0000) | ($s[2] >> 16 & 0xFF00) | ($s[3] >> 24 & 0xFF);
$s[] = ($s[4] >> 8 & 0xFF0000) | ($s[5] >> 16 & 0xFF00) | ($s[6] >> 24 & 0xFF);
$s[] = ($s[7] >> 8 & 0xFF0000) | ($s[8] >> 16 & 0xFF00) | ($s[9] >> 24 & 0xFF);
$s[] = ($s[10] >> 8 & 0xFF0000) | ($s[11] >> 16 & 0xFF00) | ($s[12] >> 24 & 0xFF);
$s[] = ($s[13] >> 8 & 0xFF0000) | ($s[14] >> 16 & 0xFF00) | ($s[15] >> 24 & 0xFF);
self::$seedParts = $s;
self::$seedIndex = 21;
}

self::$rand[5] = 0xFFFF & $carry = self::$rand[5] + (self::$seedParts[self::$seedIndex--] & 0xFFFFFF);
self::$rand[4] = 0xFFFF & $carry = self::$rand[4] + ($carry >> 16);
self::$rand[3] = 0xFFFF & $carry = self::$rand[3] + ($carry >> 16);
self::$rand[2] = 0xFFFF & $carry = self::$rand[2] + ($carry >> 16);
self::$rand[1] += $carry >> 16;

if (0xFC00 & self::$rand[1]) {
if (\PHP_INT_SIZE >= 8 || 10 > \strlen($time = self::$time)) {
$time = (string) (1 + $time);
} elseif ('999999999' === $mtime = substr($time, -9)) {
$time = (1 + substr($time, 0, -9)).'000000000';
} else {
$time = substr_replace($time, str_pad(++$mtime, 9, '0', \STR_PAD_LEFT), -9);
}

goto randomize;
}

$time = self::$time;
}

if (\PHP_INT_SIZE >= 8) {
$time = base_convert($time, 10, 16);
} else {
$time = bin2hex(BinaryUtil::fromBase($time, BinaryUtil::BASE10));
}

return substr_replace(sprintf('%012s-%04x-%04x-%04x%04x%04x',
$time,
0x7000 | (self::$rand[1] << 2) | (self::$rand[2] >> 14),
0x8000 | (self::$rand[2] & 0x3FFF),
self::$rand[3],
self::$rand[4],
self::$rand[5],
), '-', 8, 0);
}
}
Loading