diff --git a/src/Symfony/Component/Routing/Requirement/Requirement.php b/src/Symfony/Component/Routing/Requirement/Requirement.php index ccba5ec41d320..54ad86b610182 100644 --- a/src/Symfony/Component/Routing/Requirement/Requirement.php +++ b/src/Symfony/Component/Routing/Requirement/Requirement.php @@ -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}'; } diff --git a/src/Symfony/Component/Uid/CHANGELOG.md b/src/Symfony/Component/Uid/CHANGELOG.md index 7b5142ba5c7d2..b82133751c0a9 100644 --- a/src/Symfony/Component/Uid/CHANGELOG.md +++ b/src/Symfony/Component/Uid/CHANGELOG.md @@ -4,6 +4,7 @@ CHANGELOG 6.2 --- + * Add `UuidV7` and `UuidV8` * Add `TimeBasedUidInterface` to describe UIDs that embed a timestamp * Add `MaxUuid` and `MaxUlid` diff --git a/src/Symfony/Component/Uid/Factory/UuidFactory.php b/src/Symfony/Component/Uid/Factory/UuidFactory.php index aac35386e95f5..0255d51bf37b0 100644 --- a/src/Symfony/Component/Uid/Factory/UuidFactory.php +++ b/src/Symfony/Component/Uid/Factory/UuidFactory.php @@ -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; diff --git a/src/Symfony/Component/Uid/README.md b/src/Symfony/Component/Uid/README.md index 2ec5b761bc7b1..ce0fb18612d65 100644 --- a/src/Symfony/Component/Uid/README.md +++ b/src/Symfony/Component/Uid/README.md @@ -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 --------- diff --git a/src/Symfony/Component/Uid/Tests/Command/InspectUuidCommandTest.php b/src/Symfony/Component/Uid/Tests/Command/InspectUuidCommandTest.php index f581b37a7a476..c9061b5a861d0 100644 --- a/src/Symfony/Component/Uid/Tests/Command/InspectUuidCommandTest.php +++ b/src/Symfony/Component/Uid/Tests/Command/InspectUuidCommandTest.php @@ -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(<<getDisplay(true)); + } + + public function testV7() + { + $commandTester = new CommandTester(new InspectUuidCommand()); + + $this->assertSame(0, $commandTester->execute(['uuid' => '017f22e2-79b0-7cc3-98c4-dc0c0c07398f'])); + $this->assertSame(<<getDisplay(true)); + } + + public function testV8() + { + $commandTester = new CommandTester(new InspectUuidCommand()); + + $this->assertSame(0, $commandTester->execute(['uuid' => '017f22e2-79b0-8cc3-98c4-dc0c0c07398f'])); + $this->assertSame(<<getDisplay(true)); } diff --git a/src/Symfony/Component/Uid/Tests/UuidTest.php b/src/Symfony/Component/Uid/Tests/UuidTest.php index 93c2340ff9375..3ac925aa4194a 100644 --- a/src/Symfony/Component/Uid/Tests/UuidTest.php +++ b/src/Symfony/Component/Uid/Tests/UuidTest.php @@ -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 @@ -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() @@ -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); diff --git a/src/Symfony/Component/Uid/Ulid.php b/src/Symfony/Component/Uid/Ulid.php index 6a9b21cf4cc5f..81610a00d4210 100644 --- a/src/Symfony/Component/Uid/Ulid.php +++ b/src/Symfony/Component/Uid/Ulid.php @@ -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)) { @@ -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; } @@ -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'); } } diff --git a/src/Symfony/Component/Uid/Uuid.php b/src/Symfony/Component/Uid/Uuid.php index 8f763c6588a4f..da0bfcb39957a 100644 --- a/src/Symfony/Component/Uid/Uuid.php +++ b/src/Symfony/Component/Uid/Uuid.php @@ -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), }; } @@ -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)) { diff --git a/src/Symfony/Component/Uid/UuidV1.php b/src/Symfony/Component/Uid/UuidV1.php index 045f667b4c526..8c03792113741 100644 --- a/src/Symfony/Component/Uid/UuidV1.php +++ b/src/Symfony/Component/Uid/UuidV1.php @@ -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) { @@ -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; } diff --git a/src/Symfony/Component/Uid/UuidV7.php b/src/Symfony/Component/Uid/UuidV7.php new file mode 100644 index 0000000000000..718b35ff9ade7 --- /dev/null +++ b/src/Symfony/Component/Uid/UuidV7.php @@ -0,0 +1,114 @@ + + * + * 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 + */ +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); + } +} diff --git a/src/Symfony/Component/Uid/UuidV8.php b/src/Symfony/Component/Uid/UuidV8.php new file mode 100644 index 0000000000000..c194a6f699eee --- /dev/null +++ b/src/Symfony/Component/Uid/UuidV8.php @@ -0,0 +1,27 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Uid; + +/** + * A v8 UUID has no explicit requirements except embedding its version + variant bits. + * + * @author Nicolas Grekas + */ +class UuidV8 extends Uuid +{ + protected const TYPE = 8; + + public function __construct(string $uuid) + { + parent::__construct($uuid, true); + } +} diff --git a/src/Symfony/Component/Validator/Constraints/Uuid.php b/src/Symfony/Component/Validator/Constraints/Uuid.php index 82b790578378d..89067fdd47881 100644 --- a/src/Symfony/Component/Validator/Constraints/Uuid.php +++ b/src/Symfony/Component/Validator/Constraints/Uuid.php @@ -51,6 +51,8 @@ class Uuid extends Constraint public const V4_RANDOM = 4; public const V5_SHA1 = 5; public const V6_SORTABLE = 6; + public const V7_SORTABLE = 7; + public const V8_CUSTOM = 8; public const ALL_VERSIONS = [ self::V1_MAC, @@ -59,6 +61,8 @@ class Uuid extends Constraint self::V4_RANDOM, self::V5_SHA1, self::V6_SORTABLE, + self::V7_SORTABLE, + self::V8_CUSTOM, ]; /** diff --git a/src/Symfony/Component/Validator/Constraints/UuidValidator.php b/src/Symfony/Component/Validator/Constraints/UuidValidator.php index af4ae413a9b9c..c4ccfb27dee9d 100644 --- a/src/Symfony/Component/Validator/Constraints/UuidValidator.php +++ b/src/Symfony/Component/Validator/Constraints/UuidValidator.php @@ -35,7 +35,7 @@ class UuidValidator extends ConstraintValidator // Roughly speaking: // x = any hexadecimal character - // M = any allowed version {1..6} + // M = any allowed version {1..8} // N = any allowed variant {8, 9, a, b} public const STRICT_LENGTH = 36; diff --git a/src/Symfony/Component/Validator/Tests/Constraints/UuidValidatorTest.php b/src/Symfony/Component/Validator/Tests/Constraints/UuidValidatorTest.php index 347980eecea74..9a1e0183b7f02 100644 --- a/src/Symfony/Component/Validator/Tests/Constraints/UuidValidatorTest.php +++ b/src/Symfony/Component/Validator/Tests/Constraints/UuidValidatorTest.php @@ -82,6 +82,8 @@ public function getValidStrictUuids() ['456daEFb-5AA6-41B5-8DBC-068B05A8B201'], // Version 4 UUID in mixed case ['456daEFb-5AA6-41B5-8DBC-068B05A8B201', [Uuid::V4_RANDOM]], ['1eb01932-4c0b-6570-aa34-d179cdf481ae', [Uuid::V6_SORTABLE]], + ['216fff40-98d9-71e3-a5e2-0800200c9a66', [Uuid::V7_SORTABLE]], + ['216fff40-98d9-81e3-a5e2-0800200c9a66', [Uuid::V8_CUSTOM]], ]; } @@ -159,8 +161,6 @@ public function getInvalidStrictUuids() ['216fff40-98d9-11e3-a5e2-0800200c9a6', Uuid::TOO_SHORT_ERROR], ['216fff40-98d9-11e3-a5e2-0800200c9a666', Uuid::TOO_LONG_ERROR], ['216fff40-98d9-01e3-a5e2-0800200c9a66', Uuid::INVALID_VERSION_ERROR], - ['216fff40-98d9-71e3-a5e2-0800200c9a66', Uuid::INVALID_VERSION_ERROR], - ['216fff40-98d9-81e3-a5e2-0800200c9a66', Uuid::INVALID_VERSION_ERROR], ['216fff40-98d9-91e3-a5e2-0800200c9a66', Uuid::INVALID_VERSION_ERROR], ['216fff40-98d9-a1e3-a5e2-0800200c9a66', Uuid::INVALID_VERSION_ERROR], ['216fff40-98d9-b1e3-a5e2-0800200c9a66', Uuid::INVALID_VERSION_ERROR], 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