Page Menu
Home
DevCentral
Search
Configure Global Search
Log In
Files
F3766374
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Mute Notifications
Award Token
Flag For Later
Size
19 KB
Referenced Files
None
Subscribers
None
View Options
diff --git a/src/Identifiers/UUID.php b/src/Identifiers/UUID.php
index cb156e1..f13639b 100644
--- a/src/Identifiers/UUID.php
+++ b/src/Identifiers/UUID.php
@@ -1,357 +1,538 @@
<?php
declare(strict_types=1);
namespace Keruald\OmniTools\Identifiers;
use Exception;
use InvalidArgumentException;
use Keruald\OmniTools\Collections\BitsVector;
use Keruald\OmniTools\Collections\Vector;
use Keruald\OmniTools\DateTime\UUIDv1TimeStamp;
use Keruald\OmniTools\DateTime\UUIDv7TimeStamp;
+/**
+ * Allow generating and representing UUID by implementing both RFC 4122
+ * and proposed extension to UUIDv6, UUIDv7 and UUIDv8.
+ *
+ * A UUID is a universal identified with good local and global uniqueness
+ * on the form xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx where x is hexadecimal.
+ *
+ * To generate a random identified, you can use `UUID::UUIDv4()`.
+ *
+ * When you need a monotonic series of always growing identifiers,
+ * you can call `UUID::UUIDv7()`, time-dependent, with 74 bits of randomness,
+ * or `UUID::batchOfUUIDv7(10)`, with at least 62 bits of randomness warranted.
+ */
class UUID {
- const UUID_REGEXP = "/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/";
+ /**
+ * A regular expression to detect if a lowercase string is a valid UUID.
+ */
+ public const UUID_REGEXP = "/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/";
+
+ /**
+ * The maximal signed integer representable in 12 bits.
+ */
+ private const MAX_12 = 4095;
- const MAX_12 = 4095;
- const MAX_48 = 281_474_976_710_655;
- const MAX_62 = 4_611_686_018_427_387_903;
+ /**
+ * The maximal signed integer representable in 48 bits.
+ */
+ private const MAX_48 = 281_474_976_710_655;
+
+ /**
+ * The maximal signed integer representable in 62 bits.
+ */
+ private const MAX_62 = 4_611_686_018_427_387_903;
- const UUIDV7_QUANTITY_PER_MS = 63;
+ /**
+ * The quantity of UUIDv7 in a batch allowed to share the same timestamp.
+ */
+ private const UUIDV7_QUANTITY_PER_MS = 63;
///
/// Public constants from RFC 4122 and draft-peabody-dispatch-new-uuid-format-03
///
+ /**
+ * A null value for a UUID, as defined in RFC 4122.
+ */
public const NIL = "00000000-0000-0000-0000-000000000000";
+
+ /**
+ * The maximum value for a UUID.
+ */
public const MAX = "ffffffff-ffff-ffff-ffff-ffffffffffff";
///
/// RFC 4122 - UUIDv1
///
/**
+ * Generate a UUIDv1, as defined in RFC 4122.
+ *
* @param int $clk_seq_hi_res
* @param int $clk_seq_low
* @param string $mac The node information, normally the MAC address ; if
* omitted, a random value will be generated.
*
- * @return string
+ * @return string The UUID
* @throws Exception if $mac is not specified, and an appropriate source of randomness cannot be found.
* @throws InvalidArgumentException if $mac is specified and doesn't contain exactly 12 hexadecimal characters.
*/
public static function UUIDv1 (
string $mac = "",
int $clk_seq_hi_res = 0,
int $clk_seq_low = 0,
) : string {
$node = match ($mac) {
"" => BitsVector::random(48),
default => BitsVector::fromDecoratedHexString($mac),
};
return self::UUIDv1FromValues(
UUIDv1TimeStamp::now(),
$clk_seq_hi_res,
$clk_seq_low,
$node,
);
}
+ /**
+ * Generate a UUIDv1, as defined in RFC 4122, from specified values.
+ *
+ * That method can be used to reproduce a UUID from known parameters.
+ *
+ * @param UUIDv1TimeStamp $timestamp A 60 bits timestamp
+ * @param int $clk_seq_hi_res A 6 bits signed integer
+ * @param int $clk_seq_low A 8 bits signed integer
+ * @param BitsVector $node A 48 bits vector
+ *
+ * @return string The UUID
+ */
public static function UUIDv1FromValues (
UUIDv1TimeStamp $timestamp,
int $clk_seq_hi_res,
int $clk_seq_low,
BitsVector $node,
) : string {
if ($node->count() !== 48) {
throw new InvalidArgumentException("Node information must be 48 bits, ideally from a 12 characters hexadecimal MAC address string.");
}
$bits = BitsVector::new(128);
$timestamp->writeToUUIDv1($bits);
$bits->copyInteger(1, 48, 4); // version 1 from UUIDv1
$bits->copyInteger(2, 64, 2); // variant 2
$bits->copyInteger($clk_seq_hi_res, 66, 6);
$bits->copyInteger($clk_seq_low, 72, 8);
$bits->replace($node, 80, 48);
return self::reformat($bits->toHexString());
}
///
/// RFC 4122 - UUIDv4
///
/**
+ * Generate a UUIDv4, as defined in RFC 4122.
+ *
+ * This UUID offers 122 bits of randomness, built from cryptographically
+ * secure pseudo-random integers.
+ *
* @return string An RFC 4122 compliant v4 UUID
* @throws Exception if an appropriate source of randomness cannot be found.
*/
public static function UUIDv4 () : string {
// Code by Andrew Moore
// See http://php.net/manual/en/function.uniqid.php#94959
// https://www.ietf.org/rfc/rfc4122.txt
return sprintf(
'%04x%04x-%04x-%04x-%04x-%04x%04x%04x',
// 32 bits for "time_low"
random_int(0, 0xffff), random_int(0, 0xffff),
// 16 bits for "time_mid"
random_int(0, 0xffff),
// 16 bits for "time_hi_and_version",
// four most significant bits holds version number 4
random_int(0, 0x0fff) | 0x4000,
// 16 bits, 8 bits for "clk_seq_hi_res",
// 8 bits for "clk_seq_low",
// two most significant bits holds zero and one for variant DCE1.1
random_int(0, 0x3fff) | 0x8000,
// 48 bits for "node"
random_int(0, 0xffff), random_int(0, 0xffff), random_int(0, 0xffff)
);
}
+ /**
+ * Generate a UUIDv4, as defined in RFC 4122, without hyphens.
+ *
+ * @see UUID::UUIDv4()
+ * @return string
+ * @throws Exception
+ */
public static function UUIDv4WithoutHyphens () : string {
return str_replace("-", "", self::UUIDv4());
}
///
/// draft-peabody-dispatch-new-uuid-format-03 - UUIDv6
///
/**
+ * Generate a UUIDv6, as defined in draft-peabody-dispatch-new-uuid-format-03.
+ *
+ * This format is similar to UUIDv1, with bits reordered to allow
+ * monotonicity. It is mainly designed to use when compatibility with
+ * UUIDv1 is required. For new systems, UUIDv7 use is recommended.
+ *
+ * This UUID is deterministic, built from a 60 bits timestamp, a clock
+ * sequence and a node information. It doesn't contain any source of
+ * randomness, excepted if node information is replaced by 48 random bits,
+ * and as such, isn't suitable to be used to generate credentials,
+ * or an identified difficult to guess ; use UUIDv7 or UUIDv4 in such cases.
+ *
+ * The RFC 4122 recommends the use of the MAC address when available,
+ * as a good way to ensure global uniqueness of the UUID. Such use will leak
+ * your MAC address, while the warranty of global uniqueness will be false
+ * if the MAC address is spoofed, or automatically generated for a VM.
+ * As such, proposed draft don't recommend to use MAC address anymore.
+ *
* @param int $clk_seq_hi_res
* @param int $clk_seq_low
* @param string $mac The node information, normally the MAC address ; if
* omitted, a random value will be generated.
*
- * @return string
+ * @return string The UUID
* @throws Exception if $mac is not specified, and an appropriate source of randomness cannot be found.
* @throws InvalidArgumentException if $mac is specified and doesn't contain exactly 12 hexadecimal characters.
*/
public static function UUIDv6 (
string $mac = "",
int $clk_seq_hi_res = 0,
int $clk_seq_low = 0,
) : string {
$node = match ($mac) {
"" => BitsVector::random(48),
default => BitsVector::fromDecoratedHexString($mac),
};
return self::UUIDv6FromValues(
UUIDv1TimeStamp::now(),
$clk_seq_hi_res,
$clk_seq_low,
$node,
);
}
+ /**
+ * Generate a UUIDv6, as defined in draft-peabody-dispatch-new-uuid-format-03,
+ * from known values.
+ *
+ * @see UUID::UUIDv6()
+ *
+ * @param UUIDv1TimeStamp $timestamp A 60 bits precision timestamp
+ * @param int $clk_seq_hi_res
+ * @param int $clk_seq_low
+ * @param BitsVector $node A 48 bits vector to identify the node
+ *
+ * @return string The UUID
+ * @throws Exception if $mac is not specified, and an appropriate source of randomness cannot be found.
+ * @throws InvalidArgumentException if $mac is specified and doesn't contain exactly 12 hexadecimal characters.
+ */
public static function UUIDv6FromValues (
UUIDv1TimeStamp $timestamp,
int $clk_seq_hi_res,
int $clk_seq_low,
BitsVector $node,
) : string {
if ($node->count() !== 48) {
throw new InvalidArgumentException("Node information must be 48 bits, ideally from a 12 characters hexadecimal MAC address string.");
}
$bits = BitsVector::new(128);
$timestamp->writeToUUIDv6($bits);
$bits->copyInteger(6, 48, 4); // version 6 from UUIDv6
$bits->copyInteger(2, 64, 2); // variant 2
$bits->copyInteger($clk_seq_hi_res, 66, 6);
$bits->copyInteger($clk_seq_low, 72, 8);
$bits->replace($node, 80, 48);
return self::reformat($bits->toHexString());
}
+ /**
+ * Convert RFC 4122 UUIDv1 to proposed draft UUIDv6.
+
+ * @param string $uuid The UUIDv1 to convert
+ * @return string A UUIDv6 with the same information as the UUIDv1.
+ */
public static function UUIDv1ToUUIDv6 (string $uuid) : string {
$bits = BitsVector::fromDecoratedHexString($uuid);
UUIDv1TimeStamp::fromUUIDv1($uuid)->writeToUUIDv6($bits);
// Version 6 for UUIDv6, bits 48-51
$bits->copyInteger(6, 48, 4);
return self::reformat($bits->toHexString());
}
+ /**
+ * Convert proposed draft UUIDv6 to RFC 4122 UUIDv1.
+
+ * @param string $uuid The UUIDv6 to convert
+ * @return string A UUIDv1 with the same information as the UUIDv6.
+ */
public static function UUIDv6ToUUIDv1 (string $uuid) : string {
$bits = BitsVector::fromDecoratedHexString($uuid);
UUIDv1TimeStamp::fromUUIDv6($uuid)->writeToUUIDv1($bits);
// Version 1 for UUIDv6, bits 48-51
$bits->copyInteger(1, 48, 4);
return self::reformat($bits->toHexString());
}
///
/// draft-peabody-dispatch-new-uuid-format-03 - UUIDv7
///
/**
+ * Generate a UUIDv7, as defined in draft-peabody-dispatch-new-uuid-format-03.
+ *
+ * This UUID associates a 48 bits timestamp to 74 bits of randomness.
+ *
+ * When called at 1 ms intervals, it gives a monotonicity warranty, ie each
+ * UUID generated will be greater than the previous one. When you need
+ * several UUIDv7 immediately, use `UUID::batchOfUUIDv7($count)` to get
+ * the same warranty.
+ *
* @throws Exception if an appropriate source of randomness cannot be found.
- *@see UUID::batchOfUUIDv7()
+ * @see UUID::batchOfUUIDv7() when you need a monotonic series of UUIDv7
*/
public static function UUIDv7 () : string {
return self::UUIDv7FromBits(
UUIDv7TimeStamp::now()->toBitsVector(),
random_int(0, self::MAX_12),
random_int(0, self::MAX_62),
);
}
/**
- * A batch of UUIDv7 with monotonicity warranty.
+ * Generate in batch UUIDv7 with monotonicity warranty among them.
*
- * @param int $count The number of UUIDv7 to generate
+ * UUID in small batches share the same timestamp,
+ * but to maintain some bits of randomness and enough entropy,
+ * a new timestamp will be used every 64 timestamps.
*
- * @return array
+ * When generating a very large batch (> 10000), this method will be slow,
+ * as 1 ms break is needed every 64 timestamps, and random_int() can also
+ * wait for a source of entropy.
+ *
+ * @param int $count The number of UUIDv7 to generate
+ * @return string[] An array of UUIDv7 with monotonic warranty.
*/
public static function batchOfUUIDv7 (int $count) : array {
if ($count > self::UUIDV7_QUANTITY_PER_MS) {
// We only have 12 bits available in random A.
// Divide in smaller batches to avoid to touch random B.
$batch = [];
$stillToGenerateCount = $count;
while ($stillToGenerateCount > 0) {
$n = min($stillToGenerateCount, self::UUIDV7_QUANTITY_PER_MS);
array_push($batch, ...self::batchOfUUIDv7($n));
$stillToGenerateCount -= self::UUIDV7_QUANTITY_PER_MS;
usleep(1000); // That will increment the timestamp.
}
return $batch;
}
$timestamp = UUIDv7TimeStamp::now()->toBitsVector();
return self::getSeriesRandomA($count)
->map(fn($a) => self::UUIDv7FromBits(
$timestamp,
$a,
random_int(0, self::MAX_62),
))
->toArray();
}
private static function getSeriesRandomA (int $count) : Vector {
return Vector::from(Random::generateIntegerMonotonicSeries(
0, self::MAX_12, $count
));
}
-
+ /**
+ * Generate a UUIDv7 for known timestamp and known random values.
+ *
+ * @param BitsVector $unixTimestampMs A 48 bits timestamp
+ * @param int $randA A 12 bits value for random A number
+ * @param int $randB A 62 bits value for random B number
+ *
+ * @return string The UUIDv7
+ */
public static function UUIDv7FromBits (
BitsVector $unixTimestampMs,
int $randA,
int $randB
) : string {
if ($unixTimestampMs->count() != 48) {
throw new InvalidArgumentException("UUIDv7 timestamps MUST be 48 bits long.");
}
$bits = BitsVector::new(128)
->replace($unixTimestampMs, 0, 48)
->copyInteger($randA, 52, 12)
->copyInteger($randB, 66, 62)
->copyInteger(7, 48, 4) // version (bits 48 -> 51)
->copyInteger(2, 64, 2); // variant (bits 64 -> 65)
return self::reformat($bits->toHexString());
}
+ /**
+ * Allow to generate a UUIDv7 for known timestamp and known random values,
+ * when the timestamp is available as a signed 48-bits integer.
+ *
+ * @param int $unixTimestampMs A 48 bits signed integer for timestamp
+ * @param int $randA A 12 bits value for random A number
+ * @param int $randB A 62 bits value for random B number
+ *
+ * @return string The UUIDv7
+ */
public static function UUIDv7FromValues (
int $unixTimestampMs,
int $randA,
int $randB
) : string {
$bits = UUIDv7TimeStamp::fromInteger($unixTimestampMs)->toBitsVector();
return self::UUIDv7FromBits($bits, $randA, $randB);
}
///
/// draft-peabody-dispatch-new-uuid-format-03 - UUIDv6
///
/**
* Generate a UUIDv8 with three custom values.
*
- * The UUIDv8 lets the implementation decide of the bits' layout.
- * This implementation will write values like big-endian unsigned numbers.
+ * According to proposed draft, the UUIDv8 lets the implementation decides
+ * of the bits' layout. Accordingly, this method give you the control
+ * of the data you want to use in the UUID. It will write specified values
+ * like big-endian signed numbers.
+ *
+ * @param int $a A 48 bits integer for custom_a field
+ * @param int $b A 12 bits integer for custom_b field
+ * @param int $c A 62 bits integer for custom_c field
+ * @return string The generated UUID
*/
public static function UUIDv8 (int $a, int $b, int $c) : string {
if ($a > self::MAX_48) {
throw new InvalidArgumentException("custom_a field is limited to 48 bits.");
}
if ($b > self::MAX_12) {
throw new InvalidArgumentException("custom_b field is limited to 12 bits.");
}
if ($c > self::MAX_62) {
throw new InvalidArgumentException("custom_c field is limited to 62 bits.");
}
$bits = BitsVector::new(128)
->copyInteger($a, 0, 48) // bits 0 -> 47
->copyInteger($b, 52, 12) // bits 52 -> 63
->copyInteger($c, 66, 62); // bits 66 -> 127
$bits[48] = 1; // bits 48 -> 51 represent version 8 (1000)
$bits[64] = 1; // bits 64 -> 65 represent variant 2 (10)
return self::reformat($bits->toHexString());
}
///
/// Helper methods
///
+ /**
+ * Reformat a 32 or 36 hexadecimal string to a lowercase 36 uuid string.
+ *
+ * @param string $uuid a hexadecimal string with or without hyphens
+ * @return string A formatted UUID.
+ */
public static function reformat (string $uuid) : string {
$uuid = strtolower($uuid);
return match (strlen($uuid)) {
32 => implode("-", [
substr($uuid, 0, 8),
substr($uuid, 8, 4),
substr($uuid, 12, 4),
substr($uuid, 16, 4),
substr($uuid, 20, 12),
]),
36 => $uuid,
default => throw new InvalidArgumentException("UUID must be 32 or 36 characters long."),
};
}
+ /**
+ * Determine if the specified string is a valid UUID
+ *
+ * @param string A well-formatted, lowercase string
+ * @return bool
+ */
public static function isUUID ($string) : bool {
return (bool)preg_match(self::UUID_REGEXP, $string);
}
+ /**
+ * Determine the version of the specified UUID.
+ *
+ * Normally, the proposed draft recommends treating UUID as an opaque value
+ * and refrain to inspect bits. However, where necessary, inspectors method
+ * for version and variants are allowed.
+ *
+ * @param string $uuid
+ * @return int The UUID version
+ */
public static function getVersion (string $uuid) : int {
// bits 48 -> 51 represent version
return BitsVector::fromDecoratedHexString($uuid)
->slice(48, 4)
->toInteger();
}
+ /**
+ * Determine the variant of the specified UUID.
+ *
+ * Normally, the proposed draft recommends treating UUID as an opaque value
+ * and refrain to inspect bits. However, where necessary, inspectors method
+ * for version and variants are allowed.
+ *
+ * @param string $uuid
+ * @return int The UUID variant
+ */
public static function getVariant (string $uuid) : int {
// bits 64 -> 65 represent variant
return BitsVector::fromDecoratedHexString($uuid)
->slice(64, 2)
->toInteger();
}
}
File Metadata
Details
Attached
Mime Type
text/x-diff
Expires
Sun, Nov 24, 17:55 (52 m, 41 s)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
2258595
Default Alt Text
(19 KB)
Attached To
Mode
rKOT Keruald OmniTools
Attached
Detach File
Event Timeline
Log In to Comment