Passed
Push — master ( 16dc05...a881b3 )
by Fabrice
03:04
created

SoUuid   A

Complexity

Total Complexity 35

Size/Duplication

Total Lines 313
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
dl 0
loc 313
rs 9
c 0
b 0
f 0
wmc 35

18 Methods

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