├── .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 | [](https://github.com/oittaa/uuid-php/actions/workflows/main.yml)
2 | [](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 |
--------------------------------------------------------------------------------