Lines 100.00% 62 / 62
Methods 100.00% 8 / 8
Classes 100.00% 1 / 1
Name Lines Methods CRAP
 generate 100.00% 4 / 4 100.00% 1 / 1 1
 v7 100.00% 20 / 20 100.00% 1 / 1 1
 isValid 100.00% 8 / 8 100.00% 1 / 1 5
 getVersion 100.00% 5 / 5 100.00% 1 / 1 3
 v5 100.00% 10 / 10 100.00% 1 / 1 1
 toBinary 100.00% 3 / 3 100.00% 1 / 1 2
 fromBinary 100.00% 11 / 11 100.00% 1 / 1 2
 nil 100.00% 1 / 1 100.00% 1 / 1 1
16class UUID
17{
18    /**
19     * Generates a version 4 (random) UUID
20     * Follows RFC 4122 standard
21     *
22     * @return string UUID in format: xxxxxxxx-xxxx-4xxx-[89ab]xxx-xxxxxxxxxxxx
23     * @throws RandomException
24     */
25    public static function generate(): string
26    {
27        // Generate 16 bytes of random data
28        $data = random_bytes(16);
29
30        // Set version to 0100 (4)
31        $data[6] = chr(ord($data[6]) & 0x0f | 0x40);
32
33        // Set bits 6-7 to 10
34        $data[8] = chr(ord($data[8]) & 0x3f | 0x80);
35
36        // Format the bytes into a UUID string
37        return vsprintf('%s%s-%s-%s-%s-%s%s%s', str_split(bin2hex($data), 4));
38    }
39
40    /**
41     * Generates a time-ordered UUID v7
42     * Based on the draft RFC for UUIDv7 (2023)
43     * Provides improved sequential sorting while maintaining uniqueness
44     *
45     * @return string UUID in format: xxxxxxxx-xxxx-7xxx-[89ab]xxx-xxxxxxxxxxxx
46     * @throws RandomException
47     */
48    public static function v7(): string
49    {
50        // Get current Unix timestamp in milliseconds (48 bits)
51        $timestamp = (int) floor(microtime(true) * 1000);
52        $time_hex = str_pad(dechex($timestamp), 12, '0', STR_PAD_LEFT);
53
54        // Random bytes for remaining fields
55        $random = random_bytes(10);
56
57        // Format UUID fields
58        $time_low = substr($time_hex, 0, 8);
59        $time_mid = substr($time_hex, 8, 4);
60
61        // Random part as hex
62        $random_hex = bin2hex($random);
63
64        // Set version to 7 (0111)
65        $version_byte = hexdec(substr($random_hex, 0, 2)) & 0x0f | 0x70;
66        $version_hex = str_pad(dechex($version_byte), 2, '0', STR_PAD_LEFT);
67
68        // Set variant to 10xx (RFC 4122)
69        $variant_byte = hexdec(substr($random_hex, 2, 2)) & 0x3f | 0x80;
70        $variant_hex = str_pad(dechex($variant_byte), 2, '0', STR_PAD_LEFT);
71
72        return sprintf(
73            '%s-%s-%s%s-%s%s-%s',
74            $time_low,
75            $time_mid,
76            $version_hex,
77            substr($random_hex, 4, 2),
78            $variant_hex,
79            substr($random_hex, 6, 2),
80            substr($random_hex, 8, 12)
81        );
82    }
83
84    /**
85     * Validates if a string is a valid UUID
86     *
87     * @param string $uuid The string to validate
88     * @param int|null $version Specific UUID version to validate (null for any version)
89     * @return bool True if valid UUID, false otherwise
90     */
91    public static function isValid(string $uuid, ?int $version = null): bool
92    {
93        // Special case for nil UUID
94        if ($uuid === '00000000-0000-0000-0000-000000000000') {
95            return $version === null; // Nil UUID is valid only when not checking specific version
96        }
97
98        if ($version !== null) {
99            // Only standard UUID versions 1-7 are valid. Reject any other
100            // version (e.g. 9) rather than interpolating it into the regex.
101            if ($version < 1 || $version > 7) {
102                return false;
103            }
104
105            // Validate specific version
106            $pattern = '/^[0-9a-f]{8}-[0-9a-f]{4}-' . $version . '[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i';
107        } else {
108            // Validate any UUID version
109            $pattern = '/^[0-9a-f]{8}-[0-9a-f]{4}-[1-7][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i';
110        }
111
112        return preg_match($pattern, $uuid) === 1;
113    }
114
115    /**
116     * Extract the version of a UUID
117     *
118     * @param string $uuid The UUID to check
119     * @return int|null The UUID version (1-7) or null if invalid
120     */
121    public static function getVersion(string $uuid): ?int
122    {
123        // Special case for nil UUID
124        if ($uuid === '00000000-0000-0000-0000-000000000000') {
125            return 0; // Consider nil UUID as version 0
126        }
127
128        if (!self::isValid($uuid)) {
129            return null;
130        }
131
132        return (int) $uuid[14];
133    }
134
135    /**
136     * Generate a UUID with namespace (v5)
137     * Uses SHA-1 hashing algorithm
138     *
139     * @param string $namespace Namespace UUID
140     * @param string $name The name to generate a UUID for
141     * @return string UUID v5
142     */
143    public static function v5(string $namespace, string $name): string
144    {
145        // Remove hyphens and convert to binary
146        $namespace = hex2bin(str_replace('-', '', $namespace));
147
148        // Create hash
149        $hash = sha1($namespace . $name);
150
151        // Format UUID with version 5
152        return sprintf(
153            '%s-%s-%s-%s-%s',
154            substr($hash, 0, 8),
155            substr($hash, 8, 4),
156            '5' . substr($hash, 13, 3),
157            dechex(hexdec(substr($hash, 16, 2)) & 0x3f | 0x80) . substr($hash, 18, 2),
158            substr($hash, 20, 12)
159        );
160    }
161
162    /**
163     * Convert a UUID to its binary representation
164     *
165     * @param string $uuid UUID string
166     * @return string|null Binary representation or null if invalid
167     */
168    public static function toBinary(string $uuid): ?string
169    {
170        if (!self::isValid($uuid)) {
171            return null;
172        }
173
174        return hex2bin(str_replace('-', '', $uuid));
175    }
176
177    /**
178     * Convert a binary representation back to a UUID string
179     *
180     * @param string $binary Binary UUID
181     * @return string|null Formatted UUID string or null if invalid
182     */
183    public static function fromBinary(string $binary): ?string
184    {
185        if (strlen($binary) !== 16) {
186            return null;
187        }
188
189        $hex = bin2hex($binary);
190
191        return sprintf(
192            '%s-%s-%s-%s-%s',
193            substr($hex, 0, 8),
194            substr($hex, 8, 4),
195            substr($hex, 12, 4),
196            substr($hex, 16, 4),
197            substr($hex, 20, 12)
198        );
199    }
200
201    /**
202     * Generate a nil UUID (all zeros)
203     *
204     * Note: The nil UUID is a special case that should still be considered valid,
205     * despite not having the usual version/variant bits set.
206     *
207     * @return string Nil UUID
208     */
209    public static function nil(): string
210    {
211        return '00000000-0000-0000-0000-000000000000';
212    }
213
214    /**
215     * Common namespace UUIDs for use with v5()
216     *
217     * @var array
218     */
219    public static $namespaces = [
220        'dns' => '6ba7b810-9dad-11d1-80b4-00c04fd430c8',
221        'url' => '6ba7b811-9dad-11d1-80b4-00c04fd430c8',
222        'oid' => '6ba7b812-9dad-11d1-80b4-00c04fd430c8',
223        'x500' => '6ba7b814-9dad-11d1-80b4-00c04fd430c8',
224    ];
225}