Passed
Push — master ( 04d653...a5ca73 )
by Fabrice
02:21
created

SoUuid::encodeIdentifier()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 14
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

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