8000 feature #36074 [Uid] add AbstractUid and interop with base-58/32/RFC4… · symfony/symfony@ade47dd · GitHub
[go: up one dir, main page]

Skip to content

Commit ade47dd

Browse files
committed
feature #36074 [Uid] add AbstractUid and interop with base-58/32/RFC4122 encodings (nicolas-grekas)
This PR was merged into the 5.1-dev branch. Discussion ---------- [Uid] add AbstractUid and interop with base-58/32/RFC4122 encodings | Q | A | ------------- | --- | Branch? | master | Bug fix? | no | New feature? | yes | Deprecations? | no | Tickets | - | License | MIT | Doc PR | - This PR provides a base `AbstractUid` class that is shared by `Uuid` and `Ulid`. It adds new methods that provide interoperability between all types of UIDs but also between different encodings of UIDs: - `toBase58()` using the [bitcoin alphabet](https://en.wikipedia.org/wiki/Base58) - that's 22 chars aka "short-ids" - case sensitive - `toBase32()` using [Crockford's alphabet](https://www.crockford.com/base32.html) - 26 chars ids - case insensitive - `toRfc4122()` to represent as a UUID-formatted string - 36 chars This adds to `toBinary()` and to `fromString()`, the latter being able to cope with any of the 4 representations to create any kind of UIDs. Commits ------- d8479ad [Uid] add AbstractUid and interop with base-58/32/RFC4122 encodings
2 parents 1d955cc + d8479ad commit ade47dd

File tree

10 files changed

+215
-73
lines changed

10 files changed

+215
-73
lines changed
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
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+
* @experimental in 5.1
16+
*
17+
* @author Nicolas Grekas <p@tchwork.com>
18+
*/
19+
abstract class AbstractUid implements \JsonSerializable
20+
{
21+
/**
22+
* The identifier in its canonic representation.
23+
*/
24+
protected $uid;
25+
26+
/**
27+
* Whether the passed value is valid for the constructor of the current class.
28+
*/
29+
abstract public static function isValid(string $uid): bool;
30+
31+
/**
32+
* Creates an AbstractUid from an identifier represented in any of the supported formats.
33+
*
34+
* @return static
35+
*
36+
* @throws \InvalidArgumentException When the passed value is not valid
37+
*/
38+
abstract public static function fromString(string $uid): self;
39+
40+
/**
41+
* Returns the identifier as a raw binary string.
42+
*/
43+
abstract public function toBinary(): string;
44+
45+
/**
46+
* Returns the identifier as a base-58 case sensitive string.
47+
*/
48+
public function toBase58(): string
49+
{
50+
return strtr(sprintf('%022s', BinaryUtil::toBase($this->toBinary(), BinaryUtil::BASE58)), '0', '1');
51+
}
52+
53+
/**
54+
* Returns the identifier as a base-32 case insensitive string.
55+
*/
56+
public function toBase32(): string
57+
{
58+
$uid = bin2hex($this->toBinary());
59+
$uid = sprintf('%02s%04s%04s%04s%04s%04s%04s',
60+
base_convert(substr($uid, 0, 2), 16, 32),
61+
base_convert(substr($uid, 2, 5), 16, 32),
62+
base_convert(substr($uid, 7, 5), 16, 32),
63+
base_convert(substr($uid, 12, 5), 16, 32),
64+
base_convert(substr($uid, 17, 5), 16, 32),
65+
base_convert(substr($uid, 22, 5), 16, 32),
66+
base_convert(substr($uid, 27, 5), 16, 32)
67+
);
68+
69+
return strtr($uid, 'abcdefghijklmnopqrstuv', 'ABCDEFGHJKMNPQRSTVWXYZ');
70+
}
71+
72+
/**
73+
* Returns the identifier as a RFC4122 case insensitive string.
74+
*/
75+
public function toRfc4122(): string
76+
{
77+
return uuid_unparse($this->toBinary());
78+
}
79+
80+
/**
81+
* Returns whether the argument is an AbstractUid and contains the same value as the current instance.
82+
*/
83+
public function equals($other): bool
84+
{
85+
if (!$other instanceof self) {
86+
return false;
87+
}
88+
89+
return $this->uid === $other->uid;
90+
}
91+
92+
public function compare(self $other): int
93+
{
94+
return (\strlen($this->uid) - \strlen($other->uid)) ?: ($this->uid <=> $other->uid);
95+
}
96+
97+
public function __toString(): string
98+
{
99+
return $this->uid;
100+
}
101+
102+
public function jsonSerialize(): string
103+
{
104+
return $this->uid;
105+
}
106+
}

src/Symfony/Component/Uid/BinaryUtil.php

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,19 @@ class BinaryUtil
2323
0, 1, 2, 3, 4, 5, 6, 7, 8, 9,
2424
];
2525

26+
public const BASE58 = [
27+
'' => '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz',
28+
1 => 0, 1, 2, 3, 4, 5, 6, 7, 8, 'A' => 9,
29+
'B' => 10, 'C' => 11, 'D' => 12, 'E' => 13, 'F' => 14, 'G' => 15,
30+
'H' => 16, 'J' => 17, 'K' => 18, 'L' => 19, 'M' => 20, 'N' => 21,
31+
'P' => 22, 'Q' => 23, 'R' => 24, 'S' => 25, 'T' => 26, 'U' => 27,
32+
'V' => 28, 'W' => 29, 'X' => 30, 'Y' => 31, 'Z' => 32, 'a' => 33,
33+
'b' => 34, 'c' => 35, 'd' => 36, 'e' => 37, 'f' => 38, 'g' => 39,
34+
'h' => 40, 'i' => 41, 'j' => 42, 'k' => 43, 'm' => 44, 'n' => 45,
35+
'o' => 46, 'p' => 47, 'q' => 48, 'r' => 49, 's' => 50, 't' => 51,
36+
'u' => 52, 'v' => 53, 'w' => 54, 'x' => 55, 'y' => 56, 'z' => 57,
37+
];
38+
2639
public static function toBase(string $bytes, array $map): string
2740
{
2841
$base = \strlen($alphabet = $map['']);

src/Symfony/Component/Uid/NilUuid.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,6 @@ class NilUuid extends Uuid
2222

2323
public function __construct()
2424
{
25-
$this->uuid = '00000000-0000-0000-0000-000000000000';
25+
$this->uid = '00000000-0000-0000-0000-000000000000';
2626
}
2727
}

src/Symfony/Component/Uid/Tests/UlidTest.php

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313

1414
use PHPUnit\Framework\TestCase;
1515
use Symfony\Component\Uid\Ulid;
16+
use Symfony\Component\Uid\UuidV4;
1617

1718
class UlidTest extends TestCase
1819
{
@@ -49,6 +50,28 @@ public function testBinary()
4950
$this->assertTrue($ulid->equals(Ulid::fromString(hex2bin('7fffffffffffffffffffffffffffffff'))));
5051
}
5152

53+
public function testFromUuid()
54+
{
55+
$uuid = new UuidV4();
56+
57+
$ulid = Ulid::fromString($uuid);
58+
59+
$this->assertSame($uuid->toBase32(), (string) $ulid);
60+
$this->assertSame($ulid->toBase32(), (string) $ulid);
61+
$this->assertSame((string) $uuid, $ulid->toRfc4122());
62+
$this->assertTrue($ulid->equals(Ulid::fromString($uuid)));
63+
}
64+
65+
public function testBase58()
66+
{
67+
$ulid = new Ulid('00000000000000000000000000');
68+
$this->assertSame('1111111111111111111111', $ulid->toBase58());
69+
70+
$ulid = Ulid::fromString("\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF");
71+
$this->assertSame('YcVfxkQb6JRzqk5kF2tNLv', $ulid->toBase58());
72+
$this->assertTrue($ulid->equals(Ulid::fromString('YcVfxkQb6JRzqk5kF2tNLv')));
73+
}
74+
5275
/**
5376
* @group time-sensitive
5477
*/

src/Symfony/Component/Uid/Tests/UuidTest.php

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313

1414
use PHPUnit\Framework\TestCase;
1515
use Symfony\Component\Uid\NilUuid;
16+
use Symfony\Component\Uid\Ulid;
1617
use Symfony\Component\Uid\Uuid;
1718
use Symfony\Component\Uid\UuidV1;
1819
use Symfony\Component\Uid\UuidV3;
@@ -95,6 +96,26 @@ public function testBinary()
9596
$this->assertSame(self::A_UUID_V4, (string) $uuid);
9697
}
9798

99+
public function testFromUlid()
100+
{
101+
$ulid = new Ulid();
102+
$uuid = Uuid::fromString($ulid);
103+
104+
$this->assertSame((string) $ulid, $uuid->toBase32());
105+
$this->assertSame((string) $uuid, $uuid->toRfc4122());
106+
$this->assertTrue($uuid->equals(Uuid::fromString($ulid)));
107+
}
108+
109+
public function testBase58()
110+
{
111+
$uuid = new NilUuid();
112+
$this->assertSame('1111111111111111111111', $uuid->toBase58());
113+
114+
$uuid = Uuid::fromString("\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF");
115+
$this->assertSame('YcVfxkQb6JRzqk5kF2tNLv', $uuid->toBase58());
116+
$this->assertTrue($uuid->equals(Uuid::fromString('YcVfxkQb6JRzqk5kF2tNLv')));
117+
}
118+
98119
public function testIsValid()
99120
{
100121
$this->assertFalse(Uuid::isValid('not a uuid'));

src/Symfony/Component/Uid/Ulid.php

Lines changed: 18 additions & 33 deletions
< 10000 td data-grid-cell-id="diff-0d62fe678e4e29ad387346e73f0326b36ac0aa12f180810a18562efcab4602d1-45-43-2" data-line-anchor="diff-0d62fe678e4e29ad387346e73f0326b36ac0aa12f180810a18562efcab4602d1R43" data-selected="false" role="gridcell" style="background-color:var(--bgColor-default);padding-right:24px" tabindex="-1" valign="top" class="focusable-grid-cell diff-text-cell right-side-diff-cell left-side">
public static function isValid(string $ulid): bool
Original file line numberDiff line numberDiff line change
@@ -20,17 +20,15 @@
2020
*
2121
* @author Nicolas Grekas <p@tchwork.com>
2222
*/
23-
class Ulid implements \JsonSerializable
23+
class Ulid extends AbstractUid
2424
{
2525
private static $time = -1;
2626
private static $rand = [];
2727

28-
private $ulid;
29-
3028
public function __construct(string $ulid = null)
3129
{
3230
if (null === $ulid) {
33-
$this->ulid = self::generate();
31+
$this->uid = self::generate();
3432

3533
return;
3634
}
@@ -39,7 +37,7 @@ public function __construct(string $ulid = null)
3937
throw new \InvalidArgumentException(sprintf('Invalid ULID: "%s".', $ulid));
4038
}
4139

42-
$this->ulid = strtr($ulid, 'abcdefghjkmnpqrstvwxyz', 'ABCDEFGHJKMNPQRSTVWXYZ');
40+
$this->uid = strtr($ulid, 'abcdefghjkmnpqrstvwxyz', 'ABCDEFGHJKMNPQRSTVWXYZ');
4341
}
4442

4543
@@ -55,8 +53,17 @@ public static function isValid(string $ulid): bool
5553
return $ulid[0] <= '7';
5654
}
5755

58-
public static function fromString(string $ulid): self
56+
/**
57+
* {@inheritdoc}
58+
*/
59+
public static function fromString(string $ulid): parent
5960
{
61+
if (36 === \strlen($ulid) && Uuid::isValid($ulid)) {
62+
$ulid = Uuid::fromString($ulid)->toBinary();
63+
} elseif (22 === \strlen($ulid) && 22 === strspn($ulid, BinaryUtil::BASE58[''])) {
64+
$ulid = BinaryUtil::fromBase($ulid, BinaryUtil::BASE58);
65+
}
66+
6067
if (16 !== \strlen($ulid)) {
6168
return new static($ulid);
6269
}
@@ -75,9 +82,9 @@ public static function fromString(string $ulid): self
7582
return new self(strtr($ulid, 'abcdefghijklmnopqrstuv', 'ABCDEFGHJKMNPQRSTVWXYZ'));
7683
}
7784

78-
public function toBinary()
85+
public function toBinary(): string
7986
{
80-
$ulid = strtr($this->ulid, 'ABCDEFGHJKMNPQRSTVWXYZ', 'abcdefghijklmnopqrstuv');
87+
$ulid = strtr($this->uid, 'ABCDEFGHJKMNPQRSTVWXYZ', 'abcdefghijklmnopqrstuv');
8188

8289
$ulid = sprintf('%02s%05s%05s%05s%05s%05s%05s',
8390
base_convert(substr($ulid, 0, 2), 32, 16),
@@ -92,26 +99,14 @@ public function toBinary()
9299
return hex2bin($ulid);
93100
}
94101

95-
/**
96-
* Returns whether the argument is of class Ulid and contains the same value as the current instance.
97-
*/
98-
public function equals($other): bool
102+
public function toBase32(): string
99103
{
100-
if (!$other instanceof self) {
101-
return false;
102-
}
103-
104-
return $this->ulid === $other->ulid;
105-
}
106-
107-
public function compare(self $other): int
108-
{
109-
return $this->ulid <=> $other->ulid;
104+
return $this->uid;
110105
}
111106

112107
public function getTime(): float
113108
{
114-
$time = strtr(substr($this->ulid, 0, 10), 'ABCDEFGHJKMNPQRSTVWXYZ', 'abcdefghijklmnopqrstuv');
109+
$time = strtr(substr($this->uid, 0, 10), 'ABCDEFGHJKMNPQRSTVWXYZ', 'abcdefghijklmnopqrstuv');
115110

116111
if (\PHP_INT_SIZE >= 8) {
117112
return hexdec(base_convert($time, 32, 16)) / 1000;
@@ -126,16 +121,6 @@ public function getTime(): float
126121
return BinaryUtil::toBase(hex2bin($time), BinaryUtil::BASE10) / 1000;
127122
}
128123

129-
public function __toString(): string
130-
{
131-
return $this->ulid;
132-
}
133-
134-
public function jsonSerialize(): string
135-
{
136-
return $this->ulid;
137-
}
138-
139124
private static function generate(): string
140125
{
141126
$time = microtime(false);

0 commit comments

Comments
 (0)
0