Skip to content

Navigation Menu

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Commit 421431c

Browse filesBrowse files
committed
[Uid] Add UidFactory to create Ulid and Uuid from timestamps and randomness/nodes
1 parent c82567b commit 421431c
Copy full SHA for 421431c

File tree

7 files changed

+313
-42
lines changed
Filter options

7 files changed

+313
-42
lines changed

‎src/Symfony/Component/Uid/CHANGELOG.md

Copy file name to clipboardExpand all lines: src/Symfony/Component/Uid/CHANGELOG.md
+1Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ CHANGELOG
55
-----
66

77
* made UUIDv6 always return truly random node fields to prevent leaking the MAC of the host
8+
* added UidFactory to create Ulid and Uuid from timestamps and randomness/nodes
89

910
5.1.0
1011
-----
+148Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\Uid\Tests;
13+
14+
use PHPUnit\Framework\TestCase;
15+
use Symfony\Component\Uid\UidFactory;
16+
17+
final class UidFactoryTest extends TestCase
18+
{
19+
public function testCreateUlidWithoutArguments()
20+
{
21+
(new UidFactory())->createUlid();
22+
23+
$this->addToAssertionCount(1);
24+
}
25+
26+
/**
27+
* @dataProvider provideCreateUlid
28+
*/
29+
public function testCreateUlid(?float $expectedTime, ?string $expectedRandomness, ?string $timestamp, ?string $randomness)
30+
{
31+
$ulid = (new UidFactory())->createUlid($timestamp, $randomness);
32+
33+
if (null === $expectedTime && null === $expectedRandomness) {
34+
$this->addToAssertionCount(1);
35+
36+
return;
37+
}
38+
39+
if (null !== $expectedTime) {
40+
$this->assertSame($expectedTime, $ulid->getTime());
41+
}
42+
43+
if (null !== $expectedRandomness) {
44+
$this->assertStringEndsWith($expectedRandomness, $ulid->toBase32());
45+
}
46+
}
47+
48+
public function provideCreateUlid()
49+
{
50+
foreach ([
51+
[null, null],
52+
[0.0, '0'],
53+
] as [$expectedTimestamp, $timestamp]) {
54+
foreach ([
55+
null,
56+
'0000000000000000',
57+
'ZZZZZZZZZZZZZZZZ',
58+
'2345ABCDEFGHJKMN',
59+
] as $randomness) {
60+
yield [$expectedTimestamp, $randomness, $timestamp, $randomness];
61+
}
62+
}
63+
}
64+
65+
/**
66+
* @dataProvider provideCreateUuid
67+
*/
68+
public function testCreateUuidV1(?float $expectedTime, ?string $expectedNode, ?string $timestamp, ?string $node)
69+
{
70+
$uuid = (new UidFactory())->createUuidV1($timestamp, $node);
71+
72+
if (null === $expectedTime && null === $expectedNode) {
73+
$this->addToAssertionCount(1);
74+
75+
return;
76+
}
77+
78+
if (null !== $expectedTime) {
79+
$this->assertSame($expectedTime, $uuid->getTime());
80+
}
81+
82+
if (null !== $expectedNode) {
83+
$this->assertSame($expectedNode, $uuid->getNode());
84+
}
85+
}
86+
87+
/**
88+
* @dataProvider provideCreateUuid
89+
*/
90+
public function testCreateUuidV4(?float $expectedTime, ?string $expectedNode, ?string $timestamp, ?string $node)
91+
{
92+
$uuid = (new UidFactory())->createUuidV4($timestamp, $node);
93+
94+
if (null === $expectedTime && null === $expectedNode) {
95+
$this->addToAssertionCount(1);
96+
97+
return;
98+
}
99+
100+
if (null !== $expectedTime) {
101+
$this->assertSame($expectedTime, $uuid->getTime());
102+
}
103+
104+
if (null !== $expectedNode) {
105+
$this->assertSame($expectedNode, $uuid->getNode());
106+
}
107+
}
108+
109+
/**
110+
* @dataProvider provideCreateUuid
111+
*/
112+
public function testCreateUuidV6(?float $expectedTime, ?string $expectedNode, ?string $timestamp, ?string $node)
113+
{
114+
$uuid = (new UidFactory())->createUuidV6($timestamp, $node);
115+
116+
if (null === $expectedTime && null === $expectedNode) {
117+
$this->addToAssertionCount(1);
118+
119+
return;
120+
}
121+
122+
if (null !== $expectedTime) {
123+
$this->assertSame($expectedTime, $uuid->getTime());
124+
}
125+
126+
if (null !== $expectedNode) {
127+
$this->assertSame($expectedNode, $uuid->getNode());
128+
}
129+
}
130+
131+
public function provideCreateUuid()
132+
{
133+
foreach ([
134+
[-12219292800.0, '0'],
135+
[0, (string) 0x01b21dd213814000],
136+
[103072857660.6847, '999999999999999999999'],
137+
] as [$expectedTimestamp, $timestamp]) {
138+
foreach ([
139+
null,
140+
'000000000000',
141+
'ffffffffffff',
142+
'370a6f305c0d',
143+
] as $node) {
144+
yield [$expectedTimestamp, $node, $timestamp, $node];
145+
}
146+
}
147+
}
148+
}
+51Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\Uid;
13+
14+
final class UidFactory
15+
{
16+
/**
17+
* @param string|null $timestamp Milliseconds since the Unix epoch 1970-01-01 00:00:00
18+
* @param string|null $randomness 16 characters of the Base32 dictionary (0000000000000000 to ZZZZZZZZZZZZZZZZ)
19+
*/
20+
public function createUlid(string $timestamp = null, string $randomness = null): Ulid
21+
{
22+
return new Ulid(Ulid::generate($timestamp, $randomness));
23+
}
24+
25+
/**
26+
* @param string|null $timestamp Count of 100-nanosecond intervals since the UUID epoch 1582-10-15 00:00:00
27+
* @param string|null $node 12 characters of the Base16 (hexadecimal) dictionary (000000000000 to ffffffffffff)
28+
*/
29+
public function createUuidV1(string $timestamp = null, string $node = null): UuidV1
30+
{
31+
return new UuidV1(UuidV1::generate($timestamp, $node));
32+
}
33+
34+
/**
35+
* @param string|null $timestamp Count of 100-nanosecond intervals since the UUID epoch 1582-10-15 00:00:00
36+
* @param string|null $node 12 characters of the Base16 (hexadecimal) dictionary (000000000000 to ffffffffffff)
37+
*/
38+
public function createUuidV4(string $timestamp = null, string $node = null): UuidV4
39+
{
40+
return new UuidV4(UuidV4::generate($timestamp, $node));
41+
}
42+
43+
/**
44+
* @param string|null $timestamp Count of 100-nanosecond intervals since the UUID epoch 1582-10-15 00:00:00
45+
* @param string|null $node 12 characters of the Base16 (hexadecimal) dictionary (000000000000 to ffffffffffff)
46+
*/
47+
public function createUuidV6(string $timestamp = null, string $node = null): UuidV6
48+
{
49+
return new UuidV6(UuidV6::generate($timestamp, $node));
50+
}
51+
}

‎src/Symfony/Component/Uid/Ulid.php

Copy file name to clipboardExpand all lines: src/Symfony/Component/Uid/Ulid.php
+33-22Lines changed: 33 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -121,30 +121,37 @@ public function getTime(): float
121121
return BinaryUtil::toBase(hex2bin($time), BinaryUtil::BASE10) / 1000;
122122
}
123123

124-
private static function generate(): string
124+
/**
125+
* @internal
126+
*/
127+
final public static function generate(string $time = null, string $randomness = null): string
125128
{
126-
$time = microtime(false);
127-
$time = substr($time, 11).substr($time, 2, 3);
128-
129-
if ($time !== self::$time) {
130-
$r = unpack('nr1/nr2/nr3/nr4/nr', random_bytes(10));
131-
$r['r1'] |= ($r['r'] <<= 4) & 0xF0000;
132-
$r['r2'] |= ($r['r'] <<= 4) & 0xF0000;
133-
$r['r3'] |= ($r['r'] <<= 4) & 0xF0000;
134-
$r['r4'] |= ($r['r'] <<= 4) & 0xF0000;
135-
unset($r['r']);
136-
self::$rand = array_values($r);
137-
self::$time = $time;
138-
} elseif ([0xFFFFF, 0xFFFFF, 0xFFFFF, 0xFFFFF] === self::$rand) {
139-
usleep(100);
140-
141-
return self::generate();
142-
} else {
143-
for ($i = 3; $i >= 0 && 0xFFFFF === self::$rand[$i]; --$i) {
144-
self::$rand[$i] = 0;
145-
}
129+
if (null === $time) {
130+
$time = microtime(false);
131+
$time = substr($time, 11).substr($time, 2, 3);
132+
}
146133

147-
++self::$rand[$i];
134+
if (null == $randomness) {
135+
if ($time !== self::$time) {
136+
$r = unpack('nr1/nr2/nr3/nr4/nr', random_bytes(10));
137+
$r['r1'] |= ($r['r'] <<= 4) & 0xF0000;
138+
$r['r2'] |= ($r['r'] <<= 4) & 0xF0000;
139+
$r['r3'] |= ($r['r'] <<= 4) & 0xF0000;
140+
$r['r4'] |= ($r['r'] <<= 4) & 0xF0000;
141+
unset($r['r']);
142+
self::$rand = array_values($r);
143+
self::$time = $time;
144+
} elseif ([0xFFFFF, 0xFFFFF, 0xFFFFF, 0xFFFFF] === self::$rand) {
145+
usleep(100);
146+
147+
return self::generate();
148+
} else {
149+
for ($i = 3; $i >= 0 && 0xFFFFF === self::$rand[$i]; --$i) {
150+
self::$rand[$i] = 0;
151+
}
152+
153+
++self::$rand[$i];
154+
}
148155
}
149156

150157
if (\PHP_INT_SIZE >= 8) {
@@ -158,6 +165,10 @@ private static function generate(): string
158165
);
159166
}
160167

168+
if (null !== $randomness) {
169+
return strtr(sprintf('%010s', $time).$randomness, 'abcdefghijklmnopqrstuv', 'ABCDEFGHJKMNPQRSTVWXYZ');
170+
}
171+
161172
return strtr(sprintf('%010s%04s%04s%04s%04s',
162173
$time,
163174
base_convert(self::$rand[0], 10, 32),

‎src/Symfony/Component/Uid/UuidV1.php

Copy file name to clipboardExpand all lines: src/Symfony/Component/Uid/UuidV1.php
+25-1Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ class UuidV1 extends Uuid
2525
public function __construct(string $uuid = null)
2626
{
2727
if (null === $uuid) {
28-
$this->uid = uuid_create(static::TYPE);
28+
$this->uid = self::generate();
2929
} else {
3030
parent::__construct($uuid);
3131
}
@@ -42,4 +42,28 @@ public function getNode(): string
4242
{
4343
return uuid_mac($this->uid);
4444
}
45+
46+
/**
47+
* @internal
48+
*/
49+
final public static function generate(string $timestamp = null, string $node = null): string
50+
{
51+
$uuid = uuid_create(static::TYPE);
52+
53+
if (null !== $timestamp) {
54+
if (\PHP_INT_SIZE >= 8) {
55+
$time = str_pad(dechex($timestamp), 16, '0', \STR_PAD_LEFT);
56+
} else {
57+
$time = bin2hex(str_pad(BinaryUtil::fromBase($timestamp, BinaryUtil::BASE10), 16, "\0", \STR_PAD_LEFT));
58+
}
59+
60+
$uuid = substr($time, 8).'-'.substr($time, 4, 4).'-1'.substr($time, 1, 3).substr($uuid, 18);
61+
}
62+
63+
if (null !== $node) {
64+
$uuid = substr($uuid, 0, 24).$node;
65+
}
66+
67+
return $uuid;
68+
}
4569
}

‎src/Symfony/Component/Uid/UuidV4.php

Copy file name to clipboardExpand all lines: src/Symfony/Component/Uid/UuidV4.php
+27-3Lines changed: 27 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,14 +25,38 @@ class UuidV4 extends Uuid
2525
public function __construct(string $uuid = null)
2626
{
2727
if (null === $uuid) {
28+
$this->uid = self::generate();
29+
} else {
30+
parent::__construct($uuid);
31+
}
32+
}
33+
34+
public function getTime(): float
35+
{
36+
$time = '0'.substr($this->uid, 15, 3).substr($this->uid, 9, 4).substr($this->uid, 0, 8);
37+
38+
return BinaryUtil::timeToFloat($time);
39+
}
40+
41+
public function getNode(): string
42+
{
43+
return substr($this->uid, 24);
44+
}
45+
46+
/**
47+
* @internal
48+
*/
49+
public static function generate(string $timestamp = null, string $node = null): string
50+
{
51+
if (null === $timestamp && null === $node) {
2852
$uuid = random_bytes(16);
2953
$uuid[6] = $uuid[6] & "\x0F" | "\x4F";
3054
$uuid[8] = $uuid[8] & "\x3F" | "\x80";
3155
$uuid = bin2hex($uuid);
3256

33-
$this->uid = substr($uuid, 0, 8).'-'.substr($uuid, 8, 4).'-'.substr($uuid, 12, 4).'-'.substr($uuid, 16, 4).'-'.substr($uuid, 20, 12);
34-
} else {
35-
parent::__construct($uuid);
57+
return substr($uuid, 0, 8).'-'.substr($uuid, 8, 4).'-'.substr($uuid, 12, 4).'-'.substr($uuid, 16, 4).'-'.substr($uuid, 20, 12);
3658
}
59+
60+
return substr($uuid = UuidV1::generate($timestamp, $node), 0, 14).'4'.substr($uuid, 15);
3761
}
3862
}

0 commit comments

Comments
0 (0)
Morty Proxy This is a proxified and sanitized view of the page, visit original site.