Skip to content

Navigation Menu

Sign in
Appearance settings

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 42c76d7

Browse filesBrowse files
committed
feature #36042 [Uid] add support for Ulid (nicolas-grekas)
This PR was merged into the 5.1-dev branch. Discussion ---------- [Uid] add support for Ulid | Q | A | ------------- | --- | Branch? | master | Bug fix? | no | New feature? | yes | Deprecations? | no | Tickets | - | License | MIT | Doc PR | - ULIDs are useful alternatives to UUIDs. From https://github.com/ulid/spec: UUID can be suboptimal for many use-cases because: - It isn't the most character efficient way of encoding 128 bits of randomness - UUID v1/v2 is impractical in many environments, as it requires access to a unique, stable MAC address - UUID v3/v5 requires a unique seed and produces randomly distributed IDs, which can cause fragmentation in many data structures - UUID v4 provides no other information than randomness which can cause fragmentation in many data structures Instead, herein is proposed ULID: - 128-bit compatibility with UUID - 1.21e+24 unique ULIDs per millisecond - Lexicographically sortable! - Canonically encoded as a 26 character string, as opposed to the 36 character UUID - Uses Crockford's base32 for better efficiency and readability (5 bits per character) - Case insensitive - No special characters (URL safe) - Monotonic sort order (correctly detects and handles the same millisecond) Commits ------- 59044f9 [Uid] add support for Ulid
2 parents aed93cd + 59044f9 commit 42c76d7
Copy full SHA for 42c76d7

4 files changed

+285Lines changed: 285 additions & 0 deletions

File tree

Expand file treeCollapse file tree
Open diff view settings
Filter options
Expand file treeCollapse file tree
Open diff view settings
Collapse file

‎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
  • Display the source diff
  • Display the rich diff
Original file line numberDiff line numberDiff line change
@@ -5,4 +5,5 @@ CHANGELOG
55
-----
66

77
* added support for UUID
8+
* added support for ULID
89
* added the component
Collapse file
+98Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
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\Tests\Component\Uid;
13+
14+
use PHPUnit\Framework\TestCase;
15+
use Symfony\Component\Uid\Ulid;
16+
17+
class UlidTest extends TestCase
18+
{
19+
/**
20+
* @group time-sensitive
21+
*/
22+
public function testGenerate()
23+
{
24+
$a = new Ulid();
25+
$b = new Ulid();
26+
27+
$this->assertSame(0, strncmp($a, $b, 20));
28+
$a = base_convert(strtr(substr($a, -6), 'ABCDEFGHJKMNPQRSTVWXYZ', 'abcdefghijklmnopqrstuv'), 32, 10);
29+
$b = base_convert(strtr(substr($b, -6), 'ABCDEFGHJKMNPQRSTVWXYZ', 'abcdefghijklmnopqrstuv'), 32, 10);
30+
$this->assertSame(1, $b - $a);
31+
}
32+
33+
public function testWithInvalidUlid()
34+
{
35+
$this->expectException(\InvalidArgumentException::class);
36+
$this->expectExceptionMessage('Invalid ULID: "this is not a ulid".');
37+
38+
new Ulid('this is not a ulid');
39+
}
40+
41+
public function testBinary()
42+
{
43+
$ulid = new Ulid('00000000000000000000000000');
44+
$this->assertSame("\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0", $ulid->toBinary());
45+
46+
$ulid = new Ulid('3zzzzzzzzzzzzzzzzzzzzzzzzz');
47+
$this->assertSame('7fffffffffffffffffffffffffffffff', bin2hex($ulid->toBinary()));
48+
49+
$this->assertTrue($ulid->equals(Ulid::fromBinary(hex2bin('7fffffffffffffffffffffffffffffff'))));
50+
}
51+
52+
/**
53+
* @group time-sensitive
54+
*/
55+
public function testGetTime()
56+
{
57+
$time = microtime(false);
58+
$ulid = new Ulid();
59+
$time = substr($time, 11).substr($time, 1, 4);
60+
61+
$this->assertSame((float) $time, $ulid->getTime());
62+
}
63+
64+
public function testIsValid()
65+
{
66+
$this->assertFalse(Ulid::isValid('not a ulid'));
67+
$this->assertTrue(Ulid::isValid('00000000000000000000000000'));
68+
}
69+
70+
public function testEquals()
71+
{
72+
$a = new Ulid();
73+
$b = new Ulid();
74+
75+
$this->assertTrue($a->equals($a));
76+
$this->assertFalse($a->equals($b));
77+
$this->assertFalse($a->equals((string) $a));
78+
}
79+
80+
/**
81+
* @group time-sensitive
82+
*/
83+
public function testCompare()
84+
{
85+
$a = new Ulid();
86+
$b = new Ulid();
87+
88+
$this->assertSame(0, $a->compare($a));
89+
$this->assertLessThan(0, $a->compare($b));
90+
$this->assertGreaterThan(0, $b->compare($a));
91+
92+
usleep(1001);
93+
$c = new Ulid();
94+
95+
$this->assertLessThan(0, $b->compare($c));
96+
$this->assertGreaterThan(0, $c->compare($b));
97+
}
98+
}
Collapse file

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

Copy file name to clipboard
+182Lines changed: 182 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,182 @@
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+
/**
15+
* @see https://github.com/ulid/spec
16+
*
17+
* @experimental in 5.1
18+
*
19+
* @author Nicolas Grekas <p@tchwork.com>
20+
*/
21+
class Ulid implements \JsonSerializable
22+
{
23+
private static $time = -1;
24+
private static $rand = [];
25+
26+
private $ulid;
27+
28+
public function __construct(string $ulid = null)
29+
{
30+
if (null === $ulid) {
31+
$this->ulid = self::generate();
32+
33+
return;
34+
}
35+
36+
if (!self::isValid($ulid)) {
37+
throw new \InvalidArgumentException(sprintf('Invalid ULID: "%s".', $ulid));
38+
}
39+
40+
$this->ulid = strtr($ulid, 'abcdefghjkmnpqrstvwxyz', 'ABCDEFGHJKMNPQRSTVWXYZ');
41+
}
42+
43+
public static function isValid(string $ulid): bool
44+
{
45+
if (26 !== \strlen($ulid)) {
46+
return false;
47+
}
48+
49+
if (26 !== strspn($ulid, '0123456789ABCDEFGHJKMNPQRSTVWXYZabcdefghjkmnpqrstvwxyz')) {
50+
return false;
51+
}
52+
53+
return $ulid[0] <= '7';
54+
}
55+
56+
public static function fromBinary(string $ulid): self
57+
{
58+
if (16 !== \strlen($ulid)) {
59+
throw new \InvalidArgumentException('Invalid binary ULID.');
60+
}
61+
62+
$ulid = bin2hex($ulid);
63+
$ulid = sprintf('%02s%04s%04s%04s%04s%04s%04s',
64+
base_convert(substr($ulid, 0, 2), 16, 32),
65+
base_convert(substr($ulid, 2, 5), 16, 32),
66+
base_convert(substr($ulid, 7, 5), 16, 32),
67+
base_convert(substr($ulid, 12, 5), 16, 32),
68+
base_convert(substr($ulid, 17, 5), 16, 32),
69+
base_convert(substr($ulid, 22, 5), 16, 32),
70+
base_convert(substr($ulid, 27, 5), 16, 32)
71+
);
72+
73+
return new self(strtr($ulid, 'abcdefghijklmnopqrstuv', 'ABCDEFGHJKMNPQRSTVWXYZ'));
74+
}
75+
76+
public function toBinary()
77+
{
78+
$ulid = strtr($this->ulid, 'ABCDEFGHJKMNPQRSTVWXYZ', 'abcdefghijklmnopqrstuv');
79+
80+
$ulid = sprintf('%02s%05s%05s%05s%05s%05s%05s',
81+
base_convert(substr($ulid, 0, 2), 32, 16),
82+
base_convert(substr($ulid, 2, 4), 32, 16),
83+
base_convert(substr($ulid, 6, 4), 32, 16),
84+
base_convert(substr($ulid, 10, 4), 32, 16),
85+
base_convert(substr($ulid, 14, 4), 32, 16),
86+
base_convert(substr($ulid, 18, 4), 32, 16),
87+
base_convert(substr($ulid, 22, 4), 32, 16)
88+
);
89+
90+
return hex2bin($ulid);
91+
}
92+
93+
/**
94+
* Returns whether the argument is of class Ulid and contains the same value as the current instance.
95+
*/
96+
public function equals($other): bool
97+
{
98+
if (!$other instanceof self) {
99+
return false;
100+
}
101+
102+
return $this->ulid === $other->ulid;
103+
}
104+
105+
public function compare(self $other): int
106+
{
107+
return $this->ulid <=> $other->ulid;
108+
}
109+
110+
public function getTime(): float
111+
{
112+
$time = strtr(substr($this->ulid, 0, 10), 'ABCDEFGHJKMNPQRSTVWXYZ', 'abcdefghijklmnopqrstuv');
113+
114+
if (\PHP_INT_SIZE >= 8) {
115+
return hexdec(base_convert($time, 32, 16)) / 1000;
116+
}
117+
118+
$time = sprintf('%02s%05s%05s',
119+
base_convert(substr($time, 0, 2), 32, 16),
120+
base_convert(substr($time, 2, 4), 32, 16),
121+
base_convert(substr($time, 6, 4), 32, 16)
122+
);
123+
124+
return InternalUtil::toDecimal(hex2bin($time)) / 1000;
125+
}
126+
127+
public function __toString(): string
128+
{
129+
return $this->ulid;
130+
}
131+
132+
public function jsonSerialize(): string
133+
{
134+
return $this->ulid;
135+
}
136+
137+
private static function generate(): string
138+
{
139+
$time = microtime(false);
140+
$time = substr($time, 11).substr($time, 2, 3);
141+
142+
if ($time !== self::$time) {
143+
$r = unpack('nr1/nr2/nr3/nr4/nr', random_bytes(10));
144+
$r['r1'] |= ($r['r'] <<= 4) & 0xF0000;
145+
$r['r2'] |= ($r['r'] <<= 4) & 0xF0000;
146+
$r['r3'] |= ($r['r'] <<= 4) & 0xF0000;
147+
$r['r4'] |= ($r['r'] <<= 4) & 0xF0000;
148+
unset($r['r']);
149+
self::$rand = array_values($r);
150+
self::$time = $time;
151+
} elseif ([0xFFFFF, 0xFFFFF, 0xFFFFF, 0xFFFFF] === self::$rand) {
152+
usleep(100);
153+
154+
return self::generate();
155+
} else {
156+
for ($i = 3; $i >= 0 && 0xFFFFF === self::$rand[$i]; --$i) {
157+
self::$rand[$i] = 0;
158+
}
159+
160+
++self::$rand[$i];
161+
}
162+
163+
if (\PHP_INT_SIZE >= 8) {
164+
$time = base_convert($time, 10, 32);
165+
} else {
166+
$time = bin2hex(InternalUtil::toBinary($time));
167+
$time = sprintf('%s%04s%04s',
168+
base_convert(substr($time, 0, 2), 16, 32),
169+
base_convert(substr($time, 2, 5), 16, 32),
170+
base_convert(substr($time, 7, 5), 16, 32)
171+
);
172+
}
173+
174+
return strtr(sprintf('%010s%04s%04s%04s%04s',
175+
$time,
176+
base_convert(self::$rand[0], 10, 32),
177+
base_convert(self::$rand[1], 10, 32),
178+
base_convert(self::$rand[2], 10, 32),
179+
base_convert(self::$rand[3], 10, 32)
180+
), 'abcdefghijklmnopqrstuv', 'ABCDEFGHJKMNPQRSTVWXYZ');
181+
}
182+
}
Collapse file

‎src/Symfony/Component/Uid/composer.json‎

Copy file name to clipboardExpand all lines: src/Symfony/Component/Uid/composer.json
+4Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,10 @@
1010
"name": "Grégoire Pineau",
1111
"email": "lyrixx@lyrixx.info"
1212
},
13+
{
14+
"name": "Nicolas Grekas",
15+
"email": "p@tchwork.com"
16+
},
1317
{
1418
"name": "Symfony Community",
1519
"homepage": "https://symfony.com/contributors"

0 commit comments

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