Issues (116)

src/SoUuid.php (1 issue)

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
Unused Code Comprehensibility introduced by
41% of this comment could be valid code. Did you maybe forget this after debugging?

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.

Loading history...
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