From 72f3828ccae9413c3d93c93c77cc47177c40dbd0 Mon Sep 17 00:00:00 2001 From: oittaa <8972248+oittaa@users.noreply.github.com> Date: Sun, 28 Nov 2021 20:27:23 +0100 Subject: [PATCH] UUIDv7 (#23) --- README.md | 10 ++++++++-- src/UUID.php | 32 ++++++++++++++++++++++++++++++++ tests/UuidTest.php | 30 ++++++++++++++++++++++++++++-- 3 files changed, 68 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index a8704cf..8abc8a5 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ # uuid-php -A small PHP class for generating [RFC 4122](http://tools.ietf.org/html/rfc4122) version 3, 4, 5, and 6 ([draft](https://datatracker.ietf.org/doc/html/draft-peabody-dispatch-new-uuid-format-02)) universally unique identifiers (UUID). +A small PHP class for generating [RFC 4122](http://tools.ietf.org/html/rfc4122) version 3, 4, and 5 universally unique identifiers (UUID). Additionally supports [draft](https://datatracker.ietf.org/doc/html/draft-peabody-dispatch-new-uuid-format-02) versions 6 and 7. If all you want is a unique ID, you should call `uuid4()`. @@ -25,7 +25,7 @@ echo uuid4(); ## Installation -If you need comparison tools or sortable identifiers like in version 6, you might find this small and fast package useful. It doesn't require any other dependencies. +If you need comparison tools or sortable identifiers like in versions 6 and 7, you might find this small and fast package useful. It doesn't require any other dependencies. ```bash composer require oittaa/uuid @@ -58,6 +58,12 @@ echo $uuid6_first . "\n"; // e.g. 1ebacf4f-a4a8-68ee-b4ec-618c14d005d5 $uuid6_second = UUID::uuid6(); var_dump($uuid6_first < $uuid6_second); // bool(true) +// Generate a version 7 (lexicographically sortable) UUID +$uuid7_first = UUID::uuid7(); +echo $uuid7_first . "\n"; // e.g. 061a3d43-61d0-7cf4-bfce-753dadab55e1 +$uuid7_second = UUID::uuid7(); +var_dump($uuid7_first < $uuid7_second); // bool(true) + // Test if a given string is a valid UUID $isvalid = UUID::isValid('11a38b9a-b3da-360f-9353-a5a725514269'); var_dump($isvalid); // bool(true) diff --git a/src/UUID.php b/src/UUID.php index 36e2ece..ebd5016 100644 --- a/src/UUID.php +++ b/src/UUID.php @@ -176,6 +176,30 @@ public static function uuid6() return self::uuidFromHash($hash, 6); } + /** + * Generate a version 7 UUID. A v7 UUID is lexicographically sortable and is + * designed to encode a Unix timestamp with arbitrary sub-second precision. + * + * @return string The string standard representation of the UUID + */ + public static function uuid7() + { + $time = microtime(false); + $unixts = substr($time, 11); + $subsec = substr($time, 2, 7); + $unixts = str_pad(dechex(intval($unixts, 10)), 9, '0', \STR_PAD_LEFT); + $subsec = str_pad(dechex(intval($subsec, 10)), 6, '0', \STR_PAD_LEFT); + $time = sprintf( + '%09s%03s7%03s', + $unixts, + substr($subsec, 0, 3), + substr($subsec, -3) + ); + $bytes = random_bytes(8); + $hash = $time . bin2hex($bytes); + return self::uuidFromHash($hash, 7); + } + /** * Check if a string is a valid UUID. * @@ -276,4 +300,12 @@ public static function v6() { return self::uuid6(); } + /** + * @see UUID::uuid7() Alias + * @return string + */ + public static function v7() + { + return self::uuid7(); + } } diff --git a/tests/UuidTest.php b/tests/UuidTest.php index 3048969..dd90071 100644 --- a/tests/UuidTest.php +++ b/tests/UuidTest.php @@ -23,7 +23,7 @@ public function testCanGenerateValidVersion3() public function testCanGenerateValidVersion4() { $uuid1 = UUID::uuid4(); - for ($x = 0; $x < 10; $x++) { + for ($x = 0; $x < 1000; $x++) { $this->assertMatchesRegularExpression( '/^[0-9a-f]{8}\-[0-9a-f]{4}\-4[0-9a-f]{3}\-[89ab][0-9a-f]{3}\-[0-9a-f]{12}$/', $uuid1 @@ -48,7 +48,7 @@ public function testCanGenerateValidVersion5() public function testCanGenerateValidVersion6() { $uuid1 = UUID::uuid6(); - for ($x = 0; $x < 10; $x++) { + for ($x = 0; $x < 1000; $x++) { $this->assertMatchesRegularExpression( '/^[0-9a-f]{8}\-[0-9a-f]{4}\-6[0-9a-f]{3}\-[89ab][0-9a-f]{3}\-[0-9a-f]{12}$/', $uuid1 @@ -67,6 +67,28 @@ public function testCanGenerateValidVersion6() } } + public function testCanGenerateValidVersion7() + { + $uuid1 = UUID::uuid7(); + for ($x = 0; $x < 1000; $x++) { + $this->assertMatchesRegularExpression( + '/^[0-9a-f]{8}\-[0-9a-f]{4}\-7[0-9a-f]{3}\-[89ab][0-9a-f]{3}\-[0-9a-f]{12}$/', + $uuid1 + ); + usleep(1); + $uuid2 = UUID::uuid7(); + $this->assertGreaterThan( + $uuid1, + $uuid2 + ); + $this->assertLessThan( + 0, + UUID::cmp($uuid1, $uuid2) + ); + $uuid1 = $uuid2; + } + } + public function testCannotBeCreatedFromInvalidNamespace() { $this->expectException(\InvalidArgumentException::class); @@ -155,5 +177,9 @@ public function testCanUseAliases() 6, UUID::getVersion(UUID::v6()) ); + $this->assertSame( + 7, + UUID::getVersion(UUID::v7()) + ); } }