├── .editorconfig ├── .github ├── dependabot.yml └── workflows │ └── main.yml ├── .gitignore ├── .phpcs.xml ├── LICENSE ├── README.md ├── composer.json ├── phpbench.json ├── phpunit.xml ├── src └── UUID.php └── tests ├── Benchmark ├── UUIDComparisonBench.php └── UUIDGenerationBench.php ├── FutureTimeTest.php └── UuidTest.php /.editorconfig: -------------------------------------------------------------------------------- 1 | # https://editorconfig.org/ 2 | 3 | root = true 4 | 5 | [*] 6 | trim_trailing_whitespace = true 7 | insert_final_newline = true 8 | end_of_line = lf 9 | charset = utf-8 10 | indent_style = space 11 | 12 | [*.php] 13 | indent_size = 4 14 | 15 | [*.{yml,yaml}] 16 | indent_size = 2 17 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "github-actions" 4 | directory: "/" 5 | schedule: 6 | interval: "daily" 7 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | # Triggers the workflow on push or pull request events but only for the master branch 5 | push: 6 | branches: [ master ] 7 | pull_request: 8 | branches: [ master ] 9 | 10 | # Allows you to run this workflow manually from the Actions tab 11 | workflow_dispatch: 12 | 13 | jobs: 14 | lint: 15 | runs-on: ubuntu-24.04 16 | steps: 17 | - uses: actions/checkout@v4 18 | - name: Setup PHP 19 | uses: shivammathur/setup-php@2.34.0 20 | with: 21 | php-version: '8.3' 22 | tools: cs2pr, phpcs 23 | - name: Run phpcs 24 | run: phpcs -q --report=checkstyle src/ tests/ | cs2pr 25 | 26 | benchmark: 27 | runs-on: ubuntu-24.04 28 | strategy: 29 | matrix: 30 | php-versions: ['8.1', '8.2', '8.3', '8.4'] 31 | steps: 32 | - uses: actions/checkout@v4 33 | - name: Setup PHP 34 | uses: shivammathur/setup-php@2.34.0 35 | with: 36 | php-version: ${{ matrix.php-versions }} 37 | tools: composer, phpbench/phpbench 38 | coverage: none 39 | ini-values: memory_limit=-1 40 | - name: Install dependencies 41 | run: composer install 42 | - name: PHPBench 43 | run: phpbench run -q --report=aggregate -- 44 | 45 | 46 | unit-tests: 47 | runs-on: ubuntu-24.04 48 | strategy: 49 | matrix: 50 | php-versions: ['8.1', '8.2', '8.3', '8.4'] 51 | steps: 52 | - uses: actions/checkout@v4 53 | - name: Setup PHP 54 | uses: shivammathur/setup-php@2.34.0 55 | with: 56 | php-version: ${{ matrix.php-versions }} 57 | tools: composer, phpunit 58 | - name: Install dependencies 59 | run: composer install 60 | - name: PHPUnit 61 | run: phpunit --coverage-clover coverage.xml 62 | - name: Upload coverage to Codecov 63 | uses: codecov/codecov-action@v5 64 | with: 65 | flags: unittests 66 | token: ${{ secrets.CODECOV_TOKEN }} 67 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | composer.lock 2 | coverage.xml 3 | vendor/* 4 | phpcs-report.xml 5 | .php-cs-fixer.cache 6 | .phpunit.cache 7 | -------------------------------------------------------------------------------- /.phpcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Eero Vuojolahti 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![CI](https://github.com/oittaa/uuid-php/actions/workflows/main.yml/badge.svg)](https://github.com/oittaa/uuid-php/actions/workflows/main.yml) 2 | [![codecov](https://codecov.io/gh/oittaa/uuid-php/branch/master/graph/badge.svg?token=TZILVOSUKM)](https://codecov.io/gh/oittaa/uuid-php) 3 | 4 | # uuid-php 5 | 6 | A small PHP class for generating [RFC 9562][RFC 9562] universally unique identifiers (UUID) from version 3 to version 8. 7 | 8 | If all you want is a unique ID, you should call `uuid4()`. 9 | 10 | > Implementations SHOULD utilize UUIDv7 instead of UUIDv1 and UUIDv6 if possible. 11 | 12 | If you're regularly generating more than thousand UUIDs per second, you might want to use `uuid8()` instead of `uuid7()`. This implementation of `uuid8()` sacrifices some entropy compared to `uuid7()`, but offers 100 nanosecond granularity while being otherwise compatible. 13 | 14 | ## Minimal UUID v4 implementation 15 | 16 | Credits go to [this answer][stackoverflow uuid4] on Stackoverflow for this minimal RFC 9562 compliant solution. 17 | ```php 18 | = $unixts_ms) { 39 | $unixts_ms = $last_timestamp + 1; 40 | } 41 | $last_timestamp = $unixts_ms; 42 | $data = random_bytes(10); 43 | $data[0] = chr((ord($data[0]) & 0x0f) | 0x70); // set version 44 | $data[2] = chr((ord($data[2]) & 0x3f) | 0x80); // set variant 45 | return vsprintf( 46 | '%s%s-%s-%s-%s-%s%s%s', 47 | str_split( 48 | str_pad(dechex($unixts_ms), 12, '0', \STR_PAD_LEFT) . 49 | bin2hex($data), 50 | 4 51 | ) 52 | ); 53 | } 54 | 55 | echo uuid7(); 56 | ``` 57 | 58 | ## Installation 59 | 60 | If you need comparison tools or sortable identifiers like in versions 6, 7, and 8, you might find this small and fast package useful. It doesn't require any other dependencies. 61 | 62 | ```bash 63 | composer require oittaa/uuid 64 | ``` 65 | 66 | ## Usage 67 | 68 | ### How to generate 69 | 70 | ```php 71 | 0 if uuid1 is greater than uuid2, and 0 if they are equal. 147 | $cmp1 = UUID::cmp( 148 | '11a38b9a-b3da-360f-9353-a5a725514269', 149 | '2140a926-4a47-465c-b622-4571ad9bb378' 150 | ); 151 | var_dump($cmp1 < 0); // bool(true) 152 | 153 | $cmp2 = UUID::cmp( 154 | 'c4a760a8-dbcf-5254-a0d9-6a4474bd1b62', 155 | '2140a926-4a47-465c-b622-4571ad9bb378' 156 | ); 157 | var_dump($cmp2 > 0); // bool(true) 158 | 159 | $cmp3 = UUID::cmp( 160 | 'urn:uuid:c4a760a8-dbcf-5254-a0d9-6a4474bd1b62', 161 | '{C4A760A8-DBCF-5254-A0D9-6A4474BD1B62}' 162 | ); 163 | var_dump($cmp3 === 0); // bool(true) 164 | 165 | // Extract Unix time from versions 6, 7, and 8 as a string. 166 | $uuid6_time = UUID::getTime('1ec9414c-232a-6b00-b3c8-9e6bdeced846'); 167 | var_dump($uuid6_time); // string(18) "1645557742.0000000" 168 | $uuid7_time = UUID::getTime('017f22e2-79b0-7cc3-98c4-dc0c0c07398f'); 169 | var_dump($uuid7_time); // string(18) "1645557742.0000000" 170 | $uuid8_time = UUID::getTime('017f22e2-79b0-8cc3-98c4-dc0c0c07398f'); 171 | var_dump($uuid8_time); // string(18) "1645557742.0007977" 172 | 173 | // Extract the UUID version. 174 | $uuid_version = UUID::getVersion('2140a926-4a47-465c-b622-4571ad9bb378'); 175 | var_dump($uuid_version); // int(4) 176 | ``` 177 | 178 | ## UUIDv6 Field and Bit Layout 179 | 180 | ``` 181 | 0 1 2 3 182 | 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 183 | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 184 | | time_high | 185 | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 186 | | time_mid | time_low_and_version | 187 | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 188 | |clk_seq_hi_res | clk_seq_low | node (0-1) | 189 | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 190 | | node (2-5) | 191 | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 192 | ``` 193 | 194 | ## UUIDv7 Field and Bit Layout 195 | 196 | ``` 197 | 198 | 0 1 2 3 199 | 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 200 | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 201 | | unix_ts_ms | 202 | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 203 | | unix_ts_ms | ver | rand_a | 204 | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 205 | |var| rand_b | 206 | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 207 | | rand_b | 208 | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 209 | ``` 210 | 211 | ## UUIDv8 Field and Bit Layout 212 | 213 | ``` 214 | 0 1 2 3 215 | 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 216 | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 217 | | unix_ts_ms | 218 | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 219 | | unix_ts_ms | ver | subsec | 220 | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 221 | |var|sub| rand | 222 | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 223 | | rand | 224 | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 225 | ``` 226 | 227 | - `unix_ts_ms`: 48 bit big-endian unsigned number of Unix epoch timestamp with millisecond level of precision 228 | - `ver`: The 4 bit UUIDv8 version (1000) 229 | - `subsec`: 12 bits allocated to sub-second precision values 230 | - `var`: 2 bit UUID variant (10) 231 | - `sub`: 2 bits allocated to sub-second precision values 232 | - `rand`: The remaining 60 bits are filled with pseudo-random data 233 | 234 | 14 bits dedicated to sub-second precision provide 100 nanosecond resolution. The `unix_ts_ms` and `subsec` fields guarantee the order of UUIDs generated within the same timestamp by monotonically incrementing the timer. 235 | 236 | [RFC 9562]: https://datatracker.ietf.org/doc/rfc9562/ 237 | [stackoverflow uuid4]: https://stackoverflow.com/a/15875555 238 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "oittaa/uuid", 3 | "type": "library", 4 | "description": "A small PHP class for generating RFC 9562 universally unique identifiers (UUID) from version 3 to version 8.", 5 | "keywords": [ 6 | "uuid", 7 | "identifier", 8 | "guid" 9 | ], 10 | "license": "MIT", 11 | "require": { 12 | "php": "^8.1" 13 | }, 14 | "require-dev": { 15 | "phpunit/phpunit": "^10.0" 16 | }, 17 | "autoload": { 18 | "psr-4": { 19 | "UUID\\": "src/" 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /phpbench.json: -------------------------------------------------------------------------------- 1 | { 2 | "runner.bootstrap": "vendor/autoload.php", 3 | "runner.path": "tests/Benchmark", 4 | "runner.retry_threshold": 5, 5 | "runner.iterations": 5, 6 | "runner.revs": 1000 7 | } 8 | -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | tests 6 | 7 | 8 | 9 | 10 | src 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/UUID.php: -------------------------------------------------------------------------------- 1 | $unixts || self::$unixts === $unixts && self::$subsec >= $subsec) { 98 | $unixts = self::$unixts; 99 | $subsec = self::$subsec; 100 | if ($subsec >= self::SUBSEC_RANGE - 1) { 101 | $subsec = 0; 102 | $unixts++; 103 | } else { 104 | $subsec++; 105 | } 106 | } 107 | self::$unixts = $unixts; 108 | self::$subsec = $subsec; 109 | return [$unixts, $subsec]; 110 | } 111 | 112 | /** @internal */ 113 | private static function getUnixTimeMs(): int 114 | { 115 | $timestamp = microtime(false); 116 | $unixts = intval(substr($timestamp, 11), 10); 117 | $unixts_ms = $unixts * 1000 + intval(substr($timestamp, 2, 3), 10); 118 | if (self::$unixts_ms >= $unixts_ms) { 119 | $unixts_ms = self::$unixts_ms + 1; 120 | } 121 | self::$unixts_ms = $unixts_ms; 122 | return $unixts_ms; 123 | } 124 | 125 | /** @internal */ 126 | private static function stripExtras(string $uuid): string 127 | { 128 | if (preg_match(self::UUID_REGEX, $uuid, $m) !== 1) { 129 | throw new \InvalidArgumentException('Invalid UUID string: ' . $uuid); 130 | } 131 | // Get hexadecimal components of UUID 132 | return strtolower($m[2] . $m[3] . $m[4] . $m[5] . $m[6]); 133 | } 134 | 135 | /** @internal */ 136 | private static function getBytes(string $uuid): string 137 | { 138 | return pack('H*', self::stripExtras($uuid)); 139 | } 140 | 141 | /** @internal */ 142 | private static function uuidFromHex(string $uhex, int $version): string 143 | { 144 | return sprintf( 145 | '%08s-%04s-%04x-%04x-%12s', 146 | // 32 bits for "time_low" 147 | substr($uhex, 0, 8), 148 | // 16 bits for "time_mid" 149 | substr($uhex, 8, 4), 150 | // 16 bits for "time_hi_and_version", 151 | // four most significant bits holds version number 152 | (hexdec(substr($uhex, 12, 4)) & 0x0fff) | $version << 12, 153 | // 16 bits, 8 bits for "clk_seq_hi_res", 154 | // 8 bits for "clk_seq_low", 155 | // two most significant bits holds zero and one for variant DCE1.1 156 | (hexdec(substr($uhex, 16, 4)) & 0x3fff) | 0x8000, 157 | // 48 bits for "node" 158 | substr($uhex, 20, 12) 159 | ); 160 | } 161 | 162 | /** @internal */ 163 | private static function encodeSubsec(int $value): int 164 | { 165 | return intdiv($value << self::V8_SUBSEC_BITS, self::V8_SUBSEC_RANGE); 166 | } 167 | 168 | /** @internal */ 169 | private static function decodeSubsec(int $value): int 170 | { 171 | return -(-$value * self::V8_SUBSEC_RANGE >> self::V8_SUBSEC_BITS); 172 | } 173 | 174 | /** 175 | * Generate a version 3 UUID based on the MD5 hash of a namespace identifier 176 | * (which is a UUID) and a name (which is a string). 177 | * 178 | * @param string $namespace The UUID namespace in which to create the named UUID 179 | * @param string $name The name to create a UUID for 180 | * @return string The string standard representation of the UUID 181 | */ 182 | public static function uuid3(string $namespace, string $name): string 183 | { 184 | $nbytes = self::getBytes($namespace); 185 | $uhex = md5($nbytes . $name); 186 | return self::uuidFromHex($uhex, 3); 187 | } 188 | 189 | /** 190 | * Generate a version 4 (random) UUID. 191 | * 192 | * @return string The string standard representation of the UUID 193 | */ 194 | public static function uuid4(): string 195 | { 196 | $uhex = bin2hex(random_bytes(16)); 197 | return self::uuidFromHex($uhex, 4); 198 | } 199 | 200 | /** 201 | * Generate a version 5 UUID based on the SHA-1 hash of a namespace 202 | * identifier (which is a UUID) and a name (which is a string). 203 | * 204 | * @param string $namespace The UUID namespace in which to create the named UUID 205 | * @param string $name The name to create a UUID for 206 | * @return string The string standard representation of the UUID 207 | */ 208 | public static function uuid5(string $namespace, string $name): string 209 | { 210 | $nbytes = self::getBytes($namespace); 211 | $uhex = sha1($nbytes . $name); 212 | return self::uuidFromHex($uhex, 5); 213 | } 214 | 215 | /** 216 | * UUID version 6 is a field-compatible version of UUIDv1, reordered for improved 217 | * DB locality. It is expected that UUIDv6 will primarily be used in contexts 218 | * where there are existing v1 UUIDs. Systems that do not involve legacy UUIDv1 219 | * SHOULD consider using UUIDv7 instead. 220 | * 221 | * @return string The string standard representation of the UUID 222 | */ 223 | public static function uuid6(): string 224 | { 225 | [$unixts, $subsec] = self::getUnixTimeSubsec(); 226 | $timestamp = $unixts * self::SUBSEC_RANGE + $subsec; 227 | $timehex = str_pad(dechex($timestamp + self::TIME_OFFSET_INT), 15, '0', \STR_PAD_LEFT); 228 | $uhex = substr_replace(substr($timehex, -15), '6', -3, 0); 229 | $uhex .= bin2hex(random_bytes(8)); 230 | return self::uuidFromHex($uhex, 6); 231 | } 232 | 233 | /** 234 | * UUID version 7 features a time-ordered value field derived from the widely 235 | * implemented and well known Unix Epoch timestamp source, the number of 236 | * milliseconds seconds since midnight 1 Jan 1970 UTC, leap seconds excluded. As 237 | * well as improved entropy characteristics over versions 1 or 6. 238 | * 239 | * Implementations SHOULD utilize UUID version 7 over UUID version 1 and 6 if 240 | * possible. 241 | * 242 | * @return string The string standard representation of the UUID 243 | */ 244 | public static function uuid7(): string 245 | { 246 | $unixtsms = self::getUnixTimeMs(); 247 | $uhex = substr(str_pad(dechex($unixtsms), 12, '0', \STR_PAD_LEFT), -12); 248 | $uhex .= bin2hex(random_bytes(10)); 249 | return self::uuidFromHex($uhex, 7); 250 | } 251 | 252 | /** 253 | * Generate a version 8 UUID. A v8 UUID is lexicographically sortable and is 254 | * designed to encode a Unix timestamp with arbitrary sub-second precision. 255 | * 256 | * @return string The string standard representation of the UUID 257 | */ 258 | public static function uuid8(): string 259 | { 260 | [$unixts, $subsec] = self::getUnixTimeSubsec(); 261 | $unixtsms = $unixts * 1000 + intdiv($subsec, self::V8_SUBSEC_RANGE); 262 | $subsec = self::encodeSubsec($subsec % self::V8_SUBSEC_RANGE); 263 | $subsecA = $subsec >> 2; 264 | $subsecB = $subsec & 0x03; 265 | $randB = random_bytes(8); 266 | $randB[0] = chr(ord($randB[0]) & 0x0f | $subsecB << 4); 267 | $uhex = substr(str_pad(dechex($unixtsms), 12, '0', \STR_PAD_LEFT), -12); 268 | $uhex .= '8' . str_pad(dechex($subsecA), 3, '0', \STR_PAD_LEFT); 269 | $uhex .= bin2hex($randB); 270 | return self::uuidFromHex($uhex, 8); 271 | } 272 | 273 | /** 274 | * Check if a string is a valid UUID. 275 | * 276 | * @param string $uuid The string UUID to test 277 | * @return boolean Returns `true` if uuid is valid, `false` otherwise 278 | */ 279 | public static function isValid(string $uuid): bool 280 | { 281 | return preg_match(self::UUID_REGEX, $uuid) === 1; 282 | } 283 | 284 | /** 285 | * Check if two UUIDs are equal. 286 | * 287 | * @param string $uuid1 The first UUID to test 288 | * @param string $uuid2 The second UUID to test 289 | * @return boolean Returns `true` if uuid1 is equal to uuid2, `false` otherwise 290 | */ 291 | public static function equals(string $uuid1, string $uuid2): bool 292 | { 293 | return self::stripExtras($uuid1) === self::stripExtras($uuid2); 294 | } 295 | 296 | /** 297 | * Returns Unix time from a UUID. 298 | * 299 | * @param string $uuid The UUID string 300 | * @return string Unix time 301 | */ 302 | public static function getTime(string $uuid): ?string 303 | { 304 | $uuid = self::stripExtras($uuid); 305 | $version = self::getVersion($uuid); 306 | $timehex = '0' . substr($uuid, 0, 12) . substr($uuid, 13, 3); 307 | $retval = null; 308 | if ($version === 6) { 309 | $retval = ''; 310 | $ts = hexdec($timehex) - self::TIME_OFFSET_INT; 311 | if ($ts < 0) { 312 | $retval = '-'; 313 | $ts = abs($ts); 314 | } 315 | $retval .= substr_replace(str_pad(strval($ts), 8, '0', \STR_PAD_LEFT), '.', -7, 0); 316 | } elseif ($version === 7) { 317 | $unixts = hexdec(substr($timehex, 0, 13)); 318 | $retval = strval($unixts * self::V7_SUBSEC_RANGE); 319 | $retval = substr_replace(str_pad($retval, 8, '0', \STR_PAD_LEFT), '.', -7, 0); 320 | } elseif ($version === 8) { 321 | $unixts = hexdec(substr($timehex, 0, 13)); 322 | $subsec = self::decodeSubsec((hexdec(substr($timehex, 13)) << 2) + (hexdec(substr($uuid, 16, 1)) & 0x03)); 323 | $retval = strval($unixts * self::V8_SUBSEC_RANGE + $subsec); 324 | $retval = substr_replace(str_pad($retval, 8, '0', \STR_PAD_LEFT), '.', -7, 0); 325 | } 326 | return $retval; 327 | } 328 | 329 | /** 330 | * Returns the UUID version. 331 | * 332 | * @param string $uuid The UUID string 333 | * @return int Version number of the UUID 334 | */ 335 | public static function getVersion(string $uuid): int 336 | { 337 | return intval(self::stripExtras($uuid)[12], 16); 338 | } 339 | 340 | /** 341 | * UUID comparison. 342 | * 343 | * @param string $uuid1 The first UUID to test 344 | * @param string $uuid2 The second UUID to test 345 | * @return int Returns < 0 if uuid1 is less than uuid2; > 0 if uuid1 is 346 | * greater than uuid2, and 0 if they are equal. 347 | */ 348 | public static function cmp(string $uuid1, string $uuid2): int 349 | { 350 | return strcmp(self::stripExtras($uuid1), self::stripExtras($uuid2)); 351 | } 352 | 353 | /** 354 | * The string standard representation of the UUID. 355 | * 356 | * @param string $uuid The UUID string 357 | * @return string The string standard representation of the UUID 358 | */ 359 | public static function toString(string $uuid): string 360 | { 361 | $uhex = self::stripExtras($uuid); 362 | return vsprintf('%s%s-%s-%s-%s-%s%s%s', str_split($uhex, 4)); 363 | } 364 | 365 | /** 366 | * @see UUID::uuid3() Alias 367 | * @return string 368 | */ 369 | public static function v3(...$args): string 370 | { 371 | return self::uuid3(...$args); 372 | } 373 | /** 374 | * @see UUID::uuid4() Alias 375 | * @return string 376 | */ 377 | public static function v4(): string 378 | { 379 | return self::uuid4(); 380 | } 381 | /** 382 | * @see UUID::uuid5() Alias 383 | * @return string 384 | */ 385 | public static function v5(...$args): string 386 | { 387 | return self::uuid5(...$args); 388 | } 389 | /** 390 | * @see UUID::uuid6() Alias 391 | * @return string 392 | */ 393 | public static function v6(): string 394 | { 395 | return self::uuid6(); 396 | } 397 | /** 398 | * @see UUID::uuid7() Alias 399 | * @return string 400 | */ 401 | public static function v7(): string 402 | { 403 | return self::uuid7(); 404 | } 405 | /** 406 | * @see UUID::uuid8() Alias 407 | * @return string 408 | */ 409 | public static function v8(): string 410 | { 411 | return self::uuid8(); 412 | } 413 | } 414 | -------------------------------------------------------------------------------- /tests/Benchmark/UUIDComparisonBench.php: -------------------------------------------------------------------------------- 1 | getProperty('unixts'); 19 | $property->setAccessible(true); 20 | $property->setValue($a, 9000000000); 21 | $property = $reflection->getProperty('subsec'); 22 | $property->setAccessible(true); 23 | $property->setValue($a, 9999990); 24 | $property = $reflection->getProperty('unixts_ms'); 25 | $property->setAccessible(true); 26 | $property->setValue($a, 9000000000090); 27 | } 28 | 29 | protected function tearDown(): void 30 | { 31 | $a = new UUID(); 32 | $reflection = new \ReflectionClass($a); 33 | $property = $reflection->getProperty('unixts'); 34 | $property->setAccessible(true); 35 | $property->setValue($a, 0); 36 | $property = $reflection->getProperty('subsec'); 37 | $property->setAccessible(true); 38 | $property->setValue($a, 0); 39 | $property = $reflection->getProperty('unixts_ms'); 40 | $property->setAccessible(true); 41 | $property->setValue($a, 0); 42 | } 43 | 44 | public function testFutureTimeVersion6() 45 | { 46 | $uuid1 = UUID::uuid6(); 47 | for ($x = 0; $x < 1000; $x++) { 48 | $uuid2 = UUID::uuid6(); 49 | $this->assertGreaterThan( 50 | $uuid1, 51 | $uuid2 52 | ); 53 | $this->assertLessThan( 54 | 0, 55 | strcmp(UUID::getTime($uuid1), UUID::getTime($uuid2)) 56 | ); 57 | $uuid1 = $uuid2; 58 | } 59 | } 60 | 61 | public function testFutureTimeVersion7() 62 | { 63 | $uuid1 = UUID::uuid7(); 64 | for ($x = 0; $x < 1000; $x++) { 65 | $uuid2 = UUID::uuid7(); 66 | $this->assertGreaterThan( 67 | $uuid1, 68 | $uuid2 69 | ); 70 | $this->assertLessThan( 71 | 0, 72 | strcmp(UUID::getTime($uuid1), UUID::getTime($uuid2)) 73 | ); 74 | $uuid1 = $uuid2; 75 | } 76 | } 77 | 78 | public function testFutureTimeVersion8() 79 | { 80 | $uuid1 = UUID::uuid8(); 81 | for ($x = 0; $x < 1000; $x++) { 82 | $uuid2 = UUID::uuid8(); 83 | $this->assertGreaterThan( 84 | $uuid1, 85 | $uuid2 86 | ); 87 | $this->assertLessThan( 88 | 0, 89 | strcmp(UUID::getTime($uuid1), UUID::getTime($uuid2)) 90 | ); 91 | $uuid1 = $uuid2; 92 | } 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /tests/UuidTest.php: -------------------------------------------------------------------------------- 1 | assertSame( 17 | '11a38b9a-b3da-360f-9353-a5a725514269', 18 | UUID::uuid3(UUID::NAMESPACE_DNS, 'php.net') 19 | ); 20 | } 21 | 22 | public function testCanGenerateValidVersion4() 23 | { 24 | $uuid1 = UUID::uuid4(); 25 | for ($x = 0; $x < 1000; $x++) { 26 | $this->assertMatchesRegularExpression( 27 | '/^[0-9a-f]{8}\-[0-9a-f]{4}\-4[0-9a-f]{3}\-[89ab][0-9a-f]{3}\-[0-9a-f]{12}$/', 28 | $uuid1 29 | ); 30 | $uuid2 = UUID::uuid4(); 31 | $this->assertNotEquals( 32 | $uuid1, 33 | $uuid2 34 | ); 35 | $uuid1 = $uuid2; 36 | } 37 | } 38 | 39 | public function testCanGenerateValidVersion5() 40 | { 41 | $this->assertSame( 42 | 'c4a760a8-dbcf-5254-a0d9-6a4474bd1b62', 43 | UUID::uuid5(UUID::NAMESPACE_DNS, 'php.net') 44 | ); 45 | } 46 | 47 | public function testCanGenerateValidVersion6() 48 | { 49 | $uuid1 = UUID::uuid6(); 50 | for ($x = 0; $x < 1000; $x++) { 51 | $this->assertMatchesRegularExpression( 52 | '/^[0-9a-f]{8}\-[0-9a-f]{4}\-6[0-9a-f]{3}\-[89ab][0-9a-f]{3}\-[0-9a-f]{12}$/', 53 | $uuid1 54 | ); 55 | $uuid2 = UUID::uuid6(); 56 | $this->assertGreaterThan( 57 | $uuid1, 58 | $uuid2 59 | ); 60 | $this->assertLessThan( 61 | 0, 62 | UUID::cmp($uuid1, $uuid2) 63 | ); 64 | $uuid1 = $uuid2; 65 | } 66 | } 67 | 68 | public function testCanGenerateValidVersion7() 69 | { 70 | $uuid1 = UUID::uuid7(); 71 | for ($x = 0; $x < 10; $x++) { 72 | $this->assertMatchesRegularExpression( 73 | '/^[0-9a-f]{8}\-[0-9a-f]{4}\-7[0-9a-f]{3}\-[89ab][0-9a-f]{3}\-[0-9a-f]{12}$/', 74 | $uuid1 75 | ); 76 | $uuid2 = UUID::uuid7(); 77 | $this->assertGreaterThan( 78 | $uuid1, 79 | $uuid2 80 | ); 81 | $this->assertLessThan( 82 | 0, 83 | UUID::cmp($uuid1, $uuid2) 84 | ); 85 | $uuid1 = $uuid2; 86 | } 87 | } 88 | 89 | public function testCanGenerateValidVersion8() 90 | { 91 | $uuid1 = UUID::uuid8(); 92 | for ($x = 0; $x < 1000; $x++) { 93 | $this->assertMatchesRegularExpression( 94 | '/^[0-9a-f]{8}\-[0-9a-f]{4}\-8[0-9a-f]{3}\-[89ab][0-9a-f]{3}\-[0-9a-f]{12}$/', 95 | $uuid1 96 | ); 97 | $uuid2 = UUID::uuid8(); 98 | $this->assertGreaterThan( 99 | $uuid1, 100 | $uuid2 101 | ); 102 | $this->assertLessThan( 103 | 0, 104 | UUID::cmp($uuid1, $uuid2) 105 | ); 106 | $uuid1 = $uuid2; 107 | } 108 | } 109 | 110 | public function testCannotBeCreatedFromInvalidNamespace() 111 | { 112 | $this->expectException(\InvalidArgumentException::class); 113 | 114 | UUID::uuid5('invalid', 'php.net'); 115 | } 116 | 117 | public function testCanValidate() 118 | { 119 | $this->assertTrue( 120 | UUID::isValid('11a38b9a-b3da-360f-9353-a5a725514269') 121 | ); 122 | $this->assertFalse( 123 | UUID::isValid('11a38b9a-b3da-360f-9353-a5a72551426') 124 | ); 125 | $this->assertFalse( 126 | UUID::isValid('11a38b9a-b3da-360f-9353-a5a7255142690') 127 | ); 128 | $this->assertTrue( 129 | UUID::isValid('urn:uuid:c4a760a8-dbcf-5254-a0d9-6a4474bd1b62') 130 | ); 131 | $this->assertTrue( 132 | UUID::isValid('{C4A760A8-DBCF-5254-A0D9-6A4474BD1B62}') 133 | ); 134 | $this->assertFalse( 135 | UUID::isValid('{C4A760A8-DBCF-5254-A0D9-6A4474BD1B62') 136 | ); 137 | $this->assertFalse( 138 | UUID::isValid('C4A760A8-DBCF-5254-A0D9-6A4474BD1B62}') 139 | ); 140 | $this->assertTrue( 141 | UUID::equals( 142 | 'urn:uuid:c4a760a8-dbcf-5254-a0d9-6a4474bd1b62', 143 | '{C4A760A8-DBCF-5254-A0D9-6A4474BD1B62}' 144 | ) 145 | ); 146 | $this->assertFalse( 147 | UUID::equals( 148 | 'c4a760a8-dbcf-5254-a0d9-6a4474bd1b62', 149 | '2140a926-4a47-465c-b622-4571ad9bb378' 150 | ) 151 | ); 152 | } 153 | 154 | public function testCanGetVersion() 155 | { 156 | $this->assertSame( 157 | 3, 158 | UUID::getVersion('11a38b9a-b3da-360f-9353-a5a725514269') 159 | ); 160 | $this->assertSame( 161 | 5, 162 | UUID::getVersion('c4a760a8-dbcf-5254-a0d9-6a4474bd1b62') 163 | ); 164 | } 165 | 166 | public function testCanCompare() 167 | { 168 | $this->assertSame( 169 | 0, 170 | UUID::cmp('c4a760a8-dbcf-5254-a0d9-6a4474bd1b62', 'C4A760A8-DBCF-5254-A0D9-6A4474BD1B62') 171 | ); 172 | $this->assertGreaterThan( 173 | 0, 174 | UUID::cmp('c4a760a8-dbcf-5254-a0d9-6a4474bd1b63', 'c4a760a8-dbcf-5254-a0d9-6a4474bd1b62') 175 | ); 176 | } 177 | 178 | public function testToString() 179 | { 180 | $this->assertSame( 181 | 'c4a760a8-dbcf-5254-a0d9-6a4474bd1b62', 182 | UUID::toString('{C4A760A8-DBCF-5254-A0D9-6A4474BD1B62}') 183 | ); 184 | } 185 | 186 | public function testCanUseAliases() 187 | { 188 | $this->assertSame( 189 | '11a38b9a-b3da-360f-9353-a5a725514269', 190 | UUID::v3(UUID::NAMESPACE_DNS, 'php.net') 191 | ); 192 | $this->assertSame( 193 | 4, 194 | UUID::getVersion(UUID::v4()) 195 | ); 196 | $this->assertSame( 197 | 'c4a760a8-dbcf-5254-a0d9-6a4474bd1b62', 198 | UUID::v5(UUID::NAMESPACE_DNS, 'php.net') 199 | ); 200 | $this->assertSame( 201 | 6, 202 | UUID::getVersion(UUID::v6()) 203 | ); 204 | $this->assertSame( 205 | 7, 206 | UUID::getVersion(UUID::v7()) 207 | ); 208 | $this->assertSame( 209 | 8, 210 | UUID::getVersion(UUID::v8()) 211 | ); 212 | } 213 | 214 | public function testKnownGetTime() 215 | { 216 | $uuid6_time = UUID::getTime('1EC9414C-232A-6B00-B3C8-9E6BDECED846'); 217 | $this->assertSame('1645557742.0000000', $uuid6_time); 218 | $uuid7_time = UUID::getTime('017F22E2-79B0-7CC3-98C4-DC0C0C07398F'); 219 | $this->assertSame('1645557742.0000000', $uuid7_time); 220 | $uuid8_time = UUID::getTime('017F22E2-79B0-8CC3-98C4-DC0C0C07398F'); 221 | $this->assertSame('1645557742.0007977', $uuid8_time); 222 | } 223 | 224 | public function testGetTimeValid() 225 | { 226 | for ($i = 1; $i <= 10; $i++) { 227 | $uuid6 = UUID::uuid6(); 228 | $this->assertEqualsWithDelta(microtime(true), UUID::getTime($uuid6), 0.001); 229 | $uuid7 = UUID::uuid7(); 230 | $this->assertEqualsWithDelta(microtime(true), UUID::getTime($uuid7), 0.01); 231 | $uuid8 = UUID::uuid8(); 232 | $this->assertEqualsWithDelta(microtime(true), UUID::getTime($uuid8), 0.001); 233 | usleep(100000); 234 | } 235 | } 236 | 237 | public function testGetTimeNull() 238 | { 239 | $uuid4_time = UUID::getTime(UUID::uuid4()); 240 | $this->assertNull($uuid4_time); 241 | } 242 | 243 | public function testGetTimeNearEpoch() 244 | { 245 | $uuid6_time = UUID::getTime('1b21dd21-3814-6001-b6fa-54fb559c5fcd'); 246 | $this->assertSame('0.0000001', $uuid6_time); 247 | } 248 | 249 | public function testGetTimeNegativeNearEpoch() 250 | { 251 | $uuid6_time = UUID::getTime('1b21dd21-3813-6fff-b678-1556dde9b80e'); 252 | $this->assertSame('-0.0000001', $uuid6_time); 253 | } 254 | 255 | public function testGetTimeZero() 256 | { 257 | $uuid6_time = UUID::getTime('00000000-0000-6000-8000-000000000000'); 258 | $this->assertSame('-12219292800.0000000', $uuid6_time); 259 | $uuid7_time = UUID::getTime('00000000-0000-7000-8000-000000000000'); 260 | $this->assertSame('0.0000000', $uuid7_time); 261 | $uuid8_time = UUID::getTime('00000000-0000-8000-8000-000000000000'); 262 | $this->assertSame('0.0000000', $uuid8_time); 263 | } 264 | 265 | public function testGetTimeMax() 266 | { 267 | $uuid6_time = UUID::getTime('ffffffff-ffff-6fff-bfff-ffffffffffff'); 268 | $this->assertSame('103072857660.6846975', $uuid6_time); 269 | $uuid7_time = UUID::getTime('ffffffff-ffff-7fff-bfff-ffffffffffff'); 270 | $this->assertSame('281474976710.6550000', $uuid7_time); 271 | $uuid8_time = UUID::getTime('ffffffff-ffff-8fff-bfff-ffffffffffff'); 272 | $this->assertSame('281474976710.6560000', $uuid8_time); 273 | } 274 | } 275 | --------------------------------------------------------------------------------