diff --git a/README.md b/README.md index b1eb864..ce33e30 100644 --- a/README.md +++ b/README.md @@ -14,6 +14,7 @@ UUID (RFC 4122 + Unofficial/Draft), ULID, Snowflake ID, Sonyflake ID, TBSL (libr ## Table of contents + * [Prerequisites](#prerequisites) * [Installation](#installation) * [Usage](#usage) @@ -32,6 +33,7 @@ UUID (RFC 4122 + Unofficial/Draft), ULID, Snowflake ID, Sonyflake ID, TBSL (libr * [TBSL ID](#tbsl-time-based-keys-with-lexicographic-sorting) * [Support](#support) * [References](#references) + ## Prerequisites @@ -48,7 +50,8 @@ composer require infocyph/uid ### UUID (Universal Unique Identifier) -The node specific UUID's `$node` parameter (1, 6, 7, 8) is optional. If not provided, it will be generated randomly. But, +The node specific UUID's `$node` parameter (1, 6, 7, 8) is optional. If not provided, it will be generated randomly. +But, if you wanna track the source of the UUIDs, you should use it (pre-define the node per server & pass it accordingly). #### UUID v1: Time-based UUID. @@ -60,11 +63,8 @@ if you wanna track the source of the UUIDs, you should use it (pre-define the no // alternatively can also use \Infocyph\UID\uuid1(); -// Get generated node, for further use -$node = \Infocyph\UID\UUID::getNode(1); - // Pass your pre-generated node (for node specific UUID) -\Infocyph\UID\UUID::v1($node); +\Infocyph\UID\UUID::v1($node); // check additional section for how to generate one ``` #### UUID v3: Namespace based UUID. @@ -98,6 +98,8 @@ $node = \Infocyph\UID\UUID::getNode(1); #### UUID v5: Namespace based UUID. +Better replacement for v3 due to better hashing algorithm (SHA1 instead of MD5). + ```php // Get v5 UUID for 'TestString' \Infocyph\UID\UUID::v5('TestString'); @@ -117,6 +119,8 @@ $node = \Infocyph\UID\UUID::getNode(1); #### UUID v6 (draft-based/unofficial): Time-based UUID. +Better replacement for v1. Provides more randomness & uniqueness. + ```php // Get v6 UUID (Time based) \Infocyph\UID\UUID::v6(); @@ -124,11 +128,8 @@ $node = \Infocyph\UID\UUID::getNode(1); // alternatively can also use \Infocyph\UID\uuid6(); -// Get generated node, for further use -$node = \Infocyph\UID\UUID::getNode(6); - // Pass your pre-generated node (for node specific UUID) -\Infocyph\UID\UUID::v6($node); +\Infocyph\UID\UUID::v6($node); // check additional section for how to generate one ``` #### UUID v7 (draft-based/unofficial): Time-based UUID. @@ -140,11 +141,8 @@ $node = \Infocyph\UID\UUID::getNode(6); // alternatively can also use \Infocyph\UID\uuid7(); -// Get generated node, for further use -$node = \Infocyph\UID\UUID::getNode(7); - // Pass your pre-generated node (for node specific UUID) -\Infocyph\UID\UUID::v7($node); +\Infocyph\UID\UUID::v7($node); // check additional section for how to generate one ``` #### UUID v8 (draft-based/unofficial): Time-based UUID. Lexicographically sortable. @@ -156,16 +154,16 @@ $node = \Infocyph\UID\UUID::getNode(7); // alternatively can also use \Infocyph\UID\uuid8(); -// Get generated node, for further use -$node = \Infocyph\UID\UUID::getNode(8); - // Pass your pre-generated node (for node specific UUID) -\Infocyph\UID\UUID::v8($node); +\Infocyph\UID\UUID::v8($node); // check additional section for how to generate one ``` #### Additional ```php +// Generate node for further use (with version: 1, 6, 7, 8) +\Infocyph\UID\UUID::getNode(); + // Parse any UUID string \Infocyph\UID\UUID::parse($uuid); // returns ['isValid', 'version', 'time', 'node'] ``` @@ -187,7 +185,7 @@ $node = \Infocyph\UID\UUID::getNode(8); ```php // Get Snowflake ID -// optionally set worker_id & datacenter_id +// optionally you can set worker_id & datacenter_id, for server/module detection \Infocyph\UID\Snowflake::generate(); // alternatively \Infocyph\UID\snowflake(); @@ -198,7 +196,7 @@ $node = \Infocyph\UID\UUID::getNode(8); // By default, the start time is set to `2020-01-01 00:00:00`, which is changeable // but if changed, this should always stay same as long as your project lives -// & must call this before any Sonyflake call (generate/parse) +// & must call this before any Snowflake call (generate/parse) \Infocyph\UID\Snowflake::setStartTimeStamp('2000-01-01 00:00:00'); ``` @@ -206,7 +204,7 @@ $node = \Infocyph\UID\UUID::getNode(8); ```php // Get Sonyflake ID -// optionally set machine_id +// optionally set machine_id, for server detection \Infocyph\UID\Sonyflake::generate(); // alternatively \Infocyph\UID\sonyflake(); @@ -222,11 +220,12 @@ $node = \Infocyph\UID\UUID::getNode(8); ``` ### TBSL: Time-Based Keys with Lexicographic Sorting + Library exclusive. ```php // Get TBSL ID -// optionally set machine_id +// optionally set machine_id, for server detection \Infocyph\UID\TBSL::generate(); // alternatively \Infocyph\UID\tbsl(); @@ -236,6 +235,26 @@ Library exclusive. \Infocyph\UID\TBSL::parse($tbsl); ``` +## Benchmark + +| Type | Total Duration (1M Generation, Single thread) | +|:---------------------------|:---------------------------------------------:| +| UUID v1 (random node) | 1.23968s | +| UUID v1 (fixed node) | 0.94521s | +| UUID v3 (custom namespace) | 0.76439s | +| UUID v4 | 0.70501s | +| UUID v5 (custom namespace) | 0.89831s | +| UUID v6 (random node) | 1.39867s | +| UUID v6 (fixed node) | 1.42344s | +| UUID v7 (random node) | 1.40466s | +| UUID v7 (fixed node) | 1.49268s | +| UUID v8 (random node) | 1.80438s | +| UUID v8 (fixed node) | 1.78257s | +| ULID | 1.79775s | +| TBSL | 0.80612s | + +_Note: Snowflake & Sonyflake not included, due to their way of work_ + ## Support Having trouble? Create an issue! diff --git a/src/UUID.php b/src/UUID.php index 3512111..ca11afa 100644 --- a/src/UUID.php +++ b/src/UUID.php @@ -33,11 +33,10 @@ class UUID private static int $secondIntervals = 10_000_000; private static int $secondIntervals78 = 10_000; private static int $timeOffset = 0x01b21dd213814000; - private static array $nodeLength = [ - 1 => 6, - 6 => 8, - 7 => 10, - 8 => 7 + private static array $randomLength = [ + 6 => 2, + 7 => 4, + 8 => 1 ]; /** @@ -59,7 +58,7 @@ public static function v1(string $node = null): string substr($time, -12, 4), substr($time, -15, 3), $clockSeq | 0x8000, - $node ?? self::getNode(1) + $node ?? self::getNode() ); } @@ -128,7 +127,7 @@ public static function v6(string $node = null): string '6', -3, 0 - ) . ($node ?? self::getNode(6)); + ) . self::prepareNode(6, $node); return self::output(6, $string); } @@ -148,7 +147,7 @@ public static function v7(string $node = null): string self::$unixTsMs = $unixTsMs; $string = substr(str_pad(dechex($unixTsMs), 12, '0', STR_PAD_LEFT), -12) - . ($node ?? self::getNode(7)); + . self::prepareNode(7, $node); return self::output(7, $string); } @@ -168,20 +167,35 @@ public static function v8(string $node = null): string $string = substr(str_pad(dechex($unixTsMs), 12, '0', STR_PAD_LEFT), -12) . '8' . str_pad(dechex($subSecA), 3, '0', STR_PAD_LEFT) . bin2hex(chr(ord(random_bytes(1)) & 0x0f | ($subSec & 0x03) << 4)) . - ($node ?? self::getNode(8)); + self::prepareNode(8, $node); return self::output(8, $string); } /** - * Generate unique hexadecimal node. + * Generates a random node string based on the given version and node. * - * @param int $version The version of the UUID. - * @return string The generated hexadecimal node. + * @param int $version The version of the node. + * @param string|null $node The node identifier. Defaults to null. + * @return string The generated node string. + * @throws Exception + */ + private static function prepareNode(int $version, string $node = null): string + { + if (!$node) { + return bin2hex(random_bytes(self::$randomLength[$version] + 6)); + } + return bin2hex(random_bytes(self::$randomLength[$version])) . $node; + } + + /** + * Generate unique node. + * + * @return string The generated node. * @throws Exception */ - public static function getNode(int $version): string + public static function getNode(): string { - return bin2hex(random_bytes(self::$nodeLength[$version])); + return bin2hex(random_bytes(6)); } /** @@ -204,14 +218,10 @@ public static function parse(string $uuid): array return $data; } - $data['version'] = (int)$uuid[14]; - - $timeNodeApplicable = in_array($data['version'], [1, 6, 7, 8]); - $data['time'] = $timeNodeApplicable ? self::getTime($uuid) : null; - $data['node'] = $timeNodeApplicable ? substr( - str_replace('-', '', $uuid), - -(self::$nodeLength[$data['version']] * 2) - ) : null; + $uuidData = explode('-', $uuid); + $data['version'] = (int)$uuidData[2][0]; + $data['time'] = in_array($data['version'], [1, 6, 7, 8]) ? self::getTime($uuidData, $data['version']) : null; + $data['node'] = $uuidData[4]; return $data; } @@ -230,14 +240,13 @@ private static function isValid(string $uuid): bool /** * Retrieves the time from the UUID. * - * @param string $uuid The UUID string to extract time from. + * @param array $uuid The UUID array to extract time from. + * @param int $version The version of the UUID. * @return DateTimeInterface The DateTimeImmutable object representing the extracted time. * @throws UUIDException|Exception */ - private static function getTime(string $uuid): DateTimeInterface + private static function getTime(array $uuid, int $version): DateTimeInterface { - $uuid = explode('-', $uuid); - $version = (int)$uuid[2][0]; $timestamp = match ($version) { 1 => substr($uuid[2], -3) . $uuid[1] . $uuid[0], 6, 8 => $uuid[0] . $uuid[1] . substr($uuid[2], -3), @@ -256,7 +265,7 @@ private static function getTime(string $uuid): DateTimeInterface (hexdec(substr('0' . $timestamp, 13)) << 2) + (hexdec($uuid[3][0]) & 0x03) ) * self::$secondIntervals78 >> 14); - $time = str_split(strval($unixTs * self::$secondIntervals78 + $subSec), 10); + $time = str_split((string)($unixTs * self::$secondIntervals78 + $subSec), 10); $time[1] = substr($time[1], 0, 6); break; default: @@ -289,8 +298,8 @@ private static function getUnixTimeSubSec(int $version = 1): array } if ( self::$unixTs[$version] > $unixTs || - self::$unixTs[$version] === $unixTs && - self::$subSec[$version] >= $subSec + (self::$unixTs[$version] === $unixTs && + self::$subSec[$version] >= $subSec) ) { $unixTs = self::$unixTs[$version]; $subSec = self::$subSec[$version]; @@ -310,12 +319,12 @@ private static function getUnixTimeSubSec(int $version = 1): array * Generates a formatted string based on the given version and string. * * @param int $version The version number. - * @param string $string The input string. + * @param string $id The string to be formatted. * @return string The formatted string. */ - private static function output(int $version, string $string): string + private static function output(int $version, string $id): string { - $string = str_split($string, 4); + $string = str_split($id, 4); return sprintf( "%08s-%04s-$version%03s-%04x-%012s", $string[0] . $string[1], @@ -330,9 +339,9 @@ private static function output(int $version, string $string): string * Resolves the given namespace. * * @param string $namespace The namespace to be resolved. - * @return string|array|bool The resolved namespace or false if it cannot be resolved. + * @return string The resolved namespace or false if it cannot be resolved. */ - private static function nsResolve(string $namespace): string|array|bool + private static function nsResolve(string $namespace): string { if (self::isValid($namespace)) { return str_replace('-', '', $namespace); @@ -341,6 +350,6 @@ private static function nsResolve(string $namespace): string|array|bool if (isset(self::$nsList[$namespace])) { return "6ba7b81" . self::$nsList[$namespace] . "9dad11d180b400c04fd430c8"; } - return false; + return ''; } }