Test Setup Failed
Push — master ( 641466...1422e9 )
by Fabrice
02:27
created

SoUuid::generate()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 20
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Importance

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