1: | <?php
|
2: | |
3: | |
4: | |
5: | |
6: | |
7: | |
8: | |
9: | |
10: |
|
11: |
|
12: | namespace Xmf;
|
13: |
|
14: | |
15: | |
16: | |
17: | |
18: | |
19: | |
20: | |
21: | |
22: |
|
23: | class Ulid
|
24: | {
|
25: | const ENCODING_CHARS = '0123456789ABCDEFGHJKMNPQRSTVWXYZ';
|
26: | const ENCODING_LENGTH = 32;
|
27: |
|
28: | |
29: | |
30: | |
31: | |
32: |
|
33: | public static function generate(bool $upperCase = true): string
|
34: | {
|
35: | $time = self::microtimeToUlidTime(\microtime(true));
|
36: | $timeChars = self::encodeTime($time);
|
37: | $randChars = self::encodeRandomness();
|
38: | $ulid = $timeChars . $randChars;
|
39: |
|
40: | $ulid = $upperCase ? \strtoupper($ulid) : \strtolower($ulid);
|
41: |
|
42: | return $ulid;
|
43: | }
|
44: |
|
45: | |
46: | |
47: | |
48: | |
49: |
|
50: | public static function encodeTime(int $time): string
|
51: | {
|
52: | $encodingCharsArray = str_split(self::ENCODING_CHARS);
|
53: | $timeChars = '';
|
54: | for ($i = 0; $i < 10; $i++) {
|
55: | $mod = \floor($time % self::ENCODING_LENGTH);
|
56: | $timeChars = $encodingCharsArray[$mod] . $timeChars;
|
57: | $time = (int)(($time - $mod) / self::ENCODING_LENGTH);
|
58: | }
|
59: | return $timeChars;
|
60: | }
|
61: |
|
62: | public static function encodeRandomness(): string
|
63: | {
|
64: | $encodingCharsArray = str_split(self::ENCODING_CHARS);
|
65: | $randomBytes = \random_bytes(10);
|
66: |
|
67: | if (false === $randomBytes) {
|
68: | throw new \RuntimeException('Failed to generate random bytes');
|
69: | }
|
70: |
|
71: | $randChars = '';
|
72: | for ($i = 0; $i < 16; $i++) {
|
73: | $randValue = \ord($randomBytes[$i % 10]);
|
74: | if (0 === $i % 2) {
|
75: | $randValue >>= 3;
|
76: | } else {
|
77: | $randValue &= 31;
|
78: | }
|
79: | $randChars .= $encodingCharsArray[$randValue];
|
80: | }
|
81: | return $randChars;
|
82: | }
|
83: |
|
84: | |
85: | |
86: | |
87: | |
88: |
|
89: | public static function decode(string $ulid): array
|
90: | {
|
91: | if (!self::isValid($ulid)) {
|
92: | throw new \InvalidArgumentException('Invalid ULID string');
|
93: | }
|
94: |
|
95: | $time = self::decodeTime($ulid);
|
96: | $rand = self::decodeRandomness($ulid);
|
97: |
|
98: | return [
|
99: | 'time' => $time,
|
100: | 'rand' => $rand,
|
101: | ];
|
102: | }
|
103: |
|
104: | |
105: | |
106: | |
107: | |
108: |
|
109: | public static function decodeTime(string $ulid): int
|
110: | {
|
111: |
|
112: |
|
113: |
|
114: | if (!self::isValid($ulid)) {
|
115: | throw new \InvalidArgumentException('Invalid ULID string');
|
116: | }
|
117: |
|
118: | $time = 0;
|
119: | for ($i = 0; $i < 10; $i++) {
|
120: | $char = $ulid[$i];
|
121: | $value = \strpos(self::ENCODING_CHARS, $char);
|
122: | $exponent = 9 - $i;
|
123: | $time += $value * \bcpow((string)self::ENCODING_LENGTH, (string)$exponent);
|
124: | }
|
125: |
|
126: | return $time;
|
127: | }
|
128: |
|
129: | |
130: | |
131: | |
132: | |
133: |
|
134: | public static function decodeRandomness(string $ulid): int
|
135: | {
|
136: | if (26 !== strlen($ulid)) {
|
137: | throw new \InvalidArgumentException('Invalid ULID length');
|
138: | }
|
139: |
|
140: | $rand = 0;
|
141: | for ($i = 10; $i < 26; $i++) {
|
142: | $char = $ulid[$i];
|
143: | $value = \strpos(self::ENCODING_CHARS, $char);
|
144: |
|
145: |
|
146: | if ($value < 0 || $value >= self::ENCODING_LENGTH) {
|
147: | throw new \InvalidArgumentException('Invalid ULID random value');
|
148: | }
|
149: | $exponent = 15 - $i;
|
150: | $rand += $value * \bcpow((string)self::ENCODING_LENGTH, (string)$exponent);
|
151: | }
|
152: |
|
153: | return $rand;
|
154: | }
|
155: |
|
156: | |
157: | |
158: | |
159: | |
160: |
|
161: | public static function isValid(string $ulid): bool
|
162: | {
|
163: |
|
164: | if (26 !== strlen($ulid)) {
|
165: | return false;
|
166: | }
|
167: |
|
168: |
|
169: | try {
|
170: | self::decodeRandomness($ulid);
|
171: | } catch (\InvalidArgumentException $e) {
|
172: | return false;
|
173: | }
|
174: |
|
175: | return true;
|
176: | }
|
177: |
|
178: | |
179: | |
180: | |
181: | |
182: |
|
183: | public static function microtimeToUlidTime(float $microtime): int
|
184: | {
|
185: | $timestamp = $microtime * 1000000;
|
186: | $unixEpoch = 946684800000000;
|
187: |
|
188: | return (int)($timestamp - $unixEpoch);
|
189: | }
|
190: | }
|
191: |
|
192: |
|
193: |
|
194: | |