Completed
Push — master ( 371462...e4c783 )
by Fabrice
02:16
created

SoUuid::fromBase62()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 9
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

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