Page Menu
Home
DevCentral
Search
Configure Global Search
Log In
Files
F3942549
UUID.php
No One
Temporary
Actions
Download File
Edit File
Delete File
View Transforms
Subscribe
Mute Notifications
Award Token
Flag For Later
Size
18 KB
Referenced Files
None
Subscribers
None
UUID.php
View Options
<?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
{
/**
* 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
;
/**
* 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
;
/**
* 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 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 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() 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
),
);
}
/**
* Generate in batch UUIDv7 with monotonicity warranty among them.
*
* 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.
*
* 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.
*
* 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-php
Expires
Thu, Dec 26, 16:28 (7 h, 53 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
2315258
Default Alt Text
UUID.php (18 KB)
Attached To
Mode
rKOT Keruald OmniTools
Attached
Detach File
Event Timeline
Log In to Comment