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 59044f9

Browse filesBrowse files
[Uid] add support for Ulid
1 parent 08bb79b commit 59044f9
Copy full SHA for 59044f9

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.