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 |
||
71 | */ |
||
72 | protected function __construct(string $uuid) |
||
73 | { |
||
74 | $this->uuid = (string) $uuid; |
||
75 | } |
||
76 | |||
77 | /** |
||
78 | * @param string|int|null $identifier |
||
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 |
||
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 |
||
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 |
||
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 |
||
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 |
||
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())) |
||
0 ignored issues
–
show
|
|||
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 |
||
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 |
Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.
The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.
This check looks for comments that seem to be mostly valid code and reports them.