Issues (116)

src/SoUuid.php (8 issues)

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
Missing parameter comment
Loading history...
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
Coding Style Documentation introduced by
Missing parameter comment
Loading history...
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
Coding Style Documentation introduced by
Missing parameter comment
Loading history...
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
Coding Style Documentation introduced by
Missing parameter comment
Loading history...
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
Coding Style Documentation introduced by
Missing parameter comment
Loading history...
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
Coding Style Documentation introduced by
Missing parameter comment
Loading history...
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
Coding Style Documentation introduced by
Missing parameter comment
Loading history...
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
Coding Style Documentation introduced by
Missing parameter comment
Loading history...
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