1 | <?php |
||
2 | |||
3 | /* |
||
4 | * This file is part of SoUuid. |
||
5 | * (c) Fabrice de Stefanis / https://github.com/fab2s/SoUuid |
||
6 | * This source file is licensed under the MIT license which you will |
||
7 | * find in the LICENSE file or at https://opensource.org/licenses/MIT |
||
8 | */ |
||
9 | |||
10 | namespace fab2s\SoUuid; |
||
11 | |||
12 | /** |
||
13 | * class SoUuid |
||
14 | */ |
||
15 | class SoUuid implements SoUuidInterface, SoUuidFactoryInterface |
||
16 | { |
||
17 | /** |
||
18 | * The identifier separator, used to handle variable length |
||
19 | */ |
||
20 | const IDENTIFIER_SEPARATOR = "\0"; |
||
21 | |||
22 | /** |
||
23 | * String format |
||
24 | */ |
||
25 | const UUID_REGEX = '`^[0-9a-f]{14}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{6}$`i'; |
||
26 | |||
27 | /** |
||
28 | * @var string |
||
29 | */ |
||
30 | protected $uuid; |
||
31 | |||
32 | /** |
||
33 | * @var \DateTimeImmutable |
||
34 | */ |
||
35 | protected $dateTime; |
||
36 | |||
37 | /** |
||
38 | * @var array |
||
39 | */ |
||
40 | protected $decoded; |
||
41 | |||
42 | /** |
||
43 | * @var string |
||
44 | */ |
||
45 | protected $identifier; |
||
46 | |||
47 | /** |
||
48 | * @var string |
||
49 | */ |
||
50 | protected $string; |
||
51 | |||
52 | /** |
||
53 | * @var string |
||
54 | */ |
||
55 | protected $microTime; |
||
56 | |||
57 | /** |
||
58 | * @var string |
||
59 | */ |
||
60 | protected $base62; |
||
61 | |||
62 | /** |
||
63 | * @var string |
||
64 | */ |
||
65 | protected $base36; |
||
66 | |||
67 | /** |
||
68 | * SoUuid constructor. |
||
69 | * |
||
70 | * @param string $uuid |
||
0 ignored issues
–
show
Coding Style
Documentation
introduced
by
![]() |
|||
71 | */ |
||
72 | protected function __construct(string $uuid) |
||
73 | { |
||
74 | $this->uuid = (string) $uuid; |
||
75 | } |
||
76 | |||
77 | /** |
||
78 | * @param string|int|null $identifier |
||
0 ignored issues
–
show
|
|||
79 | * |
||
80 | * @throws \Exception |
||
81 | * |
||
82 | * @return SoUuidInterface |
||
83 | */ |
||
84 | public static function generate($identifier = null): SoUuidInterface |
||
85 | { |
||
86 | // 7 bit micro-time |
||
87 | $uuid = static::microTimeBin(); |
||
88 | // 6 bytes identifier |
||
89 | $uuid .= static::encodeIdentifier($identifier); |
||
90 | // 3 random bytes (2^24 = 16 777 216 combinations) |
||
91 | $uuid .= random_bytes(3); |
||
92 | |||
93 | return new static($uuid); |
||
94 | } |
||
95 | |||
96 | /** |
||
97 | * @param string $uuidString |
||
0 ignored issues
–
show
|
|||
98 | * |
||
99 | * @throws \InvalidArgumentException |
||
100 | * |
||
101 | * @return SoUuidInterface |
||
102 | */ |
||
103 | public static function fromString(string $uuidString): SoUuidInterface |
||
104 | { |
||
105 | if (!preg_match(static::UUID_REGEX, $uuidString)) { |
||
106 | throw new \InvalidArgumentException('Uuid String is not valid'); |
||
107 | } |
||
108 | |||
109 | return new static(hex2bin(str_replace('-', '', $uuidString))); |
||
110 | } |
||
111 | |||
112 | /** |
||
113 | * @param string $uuidString |
||
0 ignored issues
–
show
|
|||
114 | * |
||
115 | * @throws \InvalidArgumentException |
||
116 | * |
||
117 | * @return SoUuidInterface |
||
118 | */ |
||
119 | public static function fromHex(string $uuidString): SoUuidInterface |
||
120 | { |
||
121 | if (!preg_match('`^[0-9a-f]{32}$`i', $uuidString)) { |
||
122 | throw new \InvalidArgumentException('Uuid Hex String is not valid'); |
||
123 | } |
||
124 | |||
125 | return new static(hex2bin($uuidString)); |
||
126 | } |
||
127 | |||
128 | /** |
||
129 | * @param string $uuidString |
||
0 ignored issues
–
show
|
|||
130 | * |
||
131 | * @throws \InvalidArgumentException |
||
132 | * |
||
133 | * @return SoUuidInterface |
||
134 | */ |
||
135 | public static function fromBytes(string $uuidString): SoUuidInterface |
||
136 | { |
||
137 | if (strlen($uuidString) !== 16) { |
||
138 | throw new \InvalidArgumentException('Uuid Binary String must be of length 16'); |
||
139 | } |
||
140 | |||
141 | return new static($uuidString); |
||
142 | } |
||
143 | |||
144 | /** |
||
145 | * @param string $uuidString |
||
0 ignored issues
–
show
|
|||
146 | * |
||
147 | * @throws \InvalidArgumentException |
||
148 | * |
||
149 | * @return SoUuidInterface |
||
150 | */ |
||
151 | public static function fromBase62(string $uuidString): SoUuidInterface |
||
152 | { |
||
153 | if (!ctype_alnum($uuidString)) { |
||
154 | throw new \InvalidArgumentException('Uuid Base62 String must composed of a-zA-z0-9 exclusively'); |
||
155 | } |
||
156 | |||
157 | $hex = gmp_strval(gmp_init($uuidString, 62), 16); |
||
158 | |||
159 | return new static(hex2bin(str_pad($hex, 32, '0', STR_PAD_LEFT))); |
||
160 | } |
||
161 | |||
162 | /** |
||
163 | * @param string $uuidString |
||
0 ignored issues
–
show
|
|||
164 | * |
||
165 | * @throws \InvalidArgumentException |
||
166 | * |
||
167 | * @return SoUuidInterface |
||
168 | */ |
||
169 | public static function fromBase36(string $uuidString): SoUuidInterface |
||
170 | { |
||
171 | if (!ctype_alnum($uuidString)) { |
||
172 | throw new \InvalidArgumentException('Uuid Base36 String must composed of a-z0-9 exclusively'); |
||
173 | } |
||
174 | |||
175 | $hex = gmp_strval(gmp_init($uuidString, 36), 16); |
||
176 | |||
177 | return new static(hex2bin(str_pad($hex, 32, '0', STR_PAD_LEFT))); |
||
178 | } |
||
179 | |||
180 | /** |
||
181 | * @throws \Exception |
||
182 | * |
||
183 | * @return array |
||
184 | */ |
||
185 | public function decode(): array |
||
186 | { |
||
187 | if ($this->decoded === null) { |
||
188 | $idLen = strlen($this->getIdentifier()); |
||
189 | $this->decoded = [ |
||
190 | 'microTime' => $this->getMicroTime(), |
||
191 | 'dateTime' => $this->getDateTime(), |
||
192 | 'identifier' => $this->getIdentifier(), |
||
193 | 'rand' => bin2hex(substr($this->uuid, $idLen ? 7 + $idLen : 8)), |
||
194 | ]; |
||
195 | } |
||
196 | |||
197 | return $this->decoded; |
||
198 | } |
||
199 | |||
200 | /** |
||
201 | * @return string |
||
202 | */ |
||
203 | public function getBytes(): string |
||
204 | { |
||
205 | return $this->uuid; |
||
206 | } |
||
207 | |||
208 | /** |
||
209 | * @return string |
||
210 | */ |
||
211 | public function getHex(): string |
||
212 | { |
||
213 | return bin2hex($this->uuid); |
||
214 | } |
||
215 | |||
216 | /** |
||
217 | * @return string |
||
218 | */ |
||
219 | public function getIdentifier(): string |
||
220 | { |
||
221 | if ($this->identifier === null) { |
||
222 | $this->identifier = substr($this->uuid, 7, 6); |
||
223 | $identifierNullPos = strpos($this->identifier, static::IDENTIFIER_SEPARATOR); |
||
224 | if ($identifierNullPos !== false) { |
||
225 | // set to empty string if the identifier was random |
||
226 | // as it starts with static::IDENTIFIER_SEPARATOR |
||
227 | $this->identifier = substr($this->identifier, 0, $identifierNullPos); |
||
228 | } |
||
229 | } |
||
230 | |||
231 | return $this->identifier; |
||
232 | } |
||
233 | |||
234 | /** |
||
235 | * The string format does not match RFC pattern to prevent any confusion in this form. |
||
236 | * It's still mimicking the 36 char length to match the storage requirement of the RFC |
||
237 | * in every way : 36 char string or 16 bytes binary string, also being the most efficient |
||
238 | * |
||
239 | * @return string |
||
240 | */ |
||
241 | public function getString(): string |
||
242 | { |
||
243 | if ($this->string === null) { |
||
244 | // microsecond epoch - 2/6 id bytes - 4/6 id bytes - 6/6 id bytes - 3 random bytes |
||
245 | $hex = $this->getHex(); |
||
246 | $this->string = substr($hex, 0, 14) . '-' . |
||
247 | substr($hex, 14, 4) . '-' . |
||
248 | substr($hex, 18, 4) . '-' . |
||
249 | substr($hex, 22, 4) . '-' . |
||
250 | substr($hex, 26); |
||
251 | } |
||
252 | |||
253 | return $this->string; |
||
254 | } |
||
255 | |||
256 | /** |
||
257 | * @return string |
||
258 | */ |
||
259 | public function getMicroTime(): string |
||
260 | { |
||
261 | if ($this->microTime === null) { |
||
262 | $timeBin = substr($this->uuid, 0, 7); |
||
263 | $this->microTime = base_convert(bin2hex($timeBin), 16, 10); |
||
264 | } |
||
265 | |||
266 | return $this->microTime; |
||
267 | } |
||
268 | |||
269 | /** |
||
270 | * @throws \Exception |
||
271 | * |
||
272 | * @return \DateTimeImmutable |
||
273 | */ |
||
274 | public function getDateTime(): \DateTimeImmutable |
||
275 | { |
||
276 | if ($this->dateTime === null) { |
||
277 | $this->dateTime = new \DateTimeImmutable('@' . substr($this->getMicroTime(), 0, -6)); |
||
278 | } |
||
279 | |||
280 | return $this->dateTime; |
||
281 | } |
||
282 | |||
283 | /** |
||
284 | * @return string |
||
285 | */ |
||
286 | public function getBase62(): string |
||
287 | { |
||
288 | if ($this->base62 === null) { |
||
289 | // max SoUuid = max microtime . max rem bits = 2^56 . 2^72 = 72057594037927936 . 4722366482869645213696 |
||
290 | // max SoUuid = 720575940379279364722366482869645213696 = GUvfO1q6dEMruD35q5aZKi in base 62 (22 chars) |
||
291 | $this->base62 = gmp_strval(gmp_init(bin2hex($this->uuid), 16), 62); |
||
292 | } |
||
293 | |||
294 | return $this->base62; |
||
295 | } |
||
296 | |||
297 | /** |
||
298 | * @return string |
||
299 | */ |
||
300 | public function getBase36(): string |
||
301 | { |
||
302 | if ($this->base36 === null) { |
||
303 | // max SoUuid = 720575940379279364722366482869645213696 = w3dfhtoz4u26q89wgfzwnz94w in base 36 (25 chars) |
||
304 | $this->base36 = gmp_strval(gmp_init(bin2hex($this->uuid), 16), 36); |
||
305 | } |
||
306 | |||
307 | return $this->base36; |
||
308 | } |
||
309 | |||
310 | /** |
||
311 | * @return string |
||
312 | */ |
||
313 | public static function microTimeBin(): string |
||
314 | { |
||
315 | // get real microsecond precision, as both microtime(1) and array_sum(explode(' ', microtime())) |
||
316 | // are limited by php.ini precision |
||
317 | $timeParts = explode(' ', microtime(false)); |
||
318 | $timeMicroSec = $timeParts[1] . substr($timeParts[0], 2, 6); |
||
319 | // convert to 56-bit integer (7 bytes), enough to store micro time is enough up to 4253-05-31 22:20:37 |
||
320 | $time = base_convert($timeMicroSec, 10, 16); |
||
321 | // left pad the eventual gap |
||
322 | return hex2bin(str_pad($time, 14, '0', STR_PAD_LEFT)); |
||
323 | } |
||
324 | |||
325 | /** |
||
326 | * @param string|int|null $identifier |
||
0 ignored issues
–
show
|
|||
327 | * |
||
328 | * @throws \Exception |
||
329 | * @throws \InvalidArgumentException |
||
330 | * |
||
331 | * @return string |
||
332 | */ |
||
333 | public static function encodeIdentifier($identifier = null): string |
||
334 | { |
||
335 | if ($identifier !== null) { |
||
336 | if (strpos($identifier, static::IDENTIFIER_SEPARATOR) !== false) { |
||
337 | throw new \InvalidArgumentException('SoUuid identifiers cannot contain ' . bin2hex(static::IDENTIFIER_SEPARATOR)); |
||
338 | } |
||
339 | |||
340 | $len = strlen($identifier); |
||
341 | $identifier = substr($identifier, 0, 6) . ($len <= 4 ? static::IDENTIFIER_SEPARATOR . random_bytes(5 - $len) : ''); |
||
342 | |||
343 | return str_pad($identifier, 6, static::IDENTIFIER_SEPARATOR, STR_PAD_RIGHT); |
||
344 | } |
||
345 | |||
346 | return static::IDENTIFIER_SEPARATOR . random_bytes(5); |
||
347 | } |
||
348 | } |
||
349 |