Completed
Push — master ( 26ee5a...f9c4ed )
by Tomasz
03:08 queued 16s
created

Generator::generateWithSameTime()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 9
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 6
CRAP Score 2

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 0
loc 9
ccs 6
cts 6
cp 1
rs 9.6666
cc 2
eloc 5
nc 2
nop 0
crap 2
1
<?php
2
3
/**
4
 * Like the Twitter one.
5
 * 
6
 * 64 bits:
7
 * 
8
 * time - 41 bits (millisecond precision w/ a custom epoch gives us 69 years)
9
 * configured machine id - 10 bits - gives us up to 1024 machines
10
 * sequence number - 12 bits - rolls over every 4096 per machine (with protection to avoid rollover in the same ms)
11
 * 
12
 * 32 bits + 9 = 41 bits of time
13
 * 2199023255552 < milliseconds = 2199023255 seconds
14
 *                                2147483647 < max 31 bit int (signed)
15
 *
16
 * @author @davegardnerisme
17
 */
18
19
namespace Gendoria\CruftFlake\Generator;
20
21
use Gendoria\CruftFlake\Config\ConfigInterface;
22
use Gendoria\CruftFlake\Timer\TimerInterface;
23
use InvalidArgumentException;
24
use OverflowException;
25
use UnexpectedValueException;
26
27
class Generator
28
{
29
30
    /**
31
     * Max timestamp.
32
     */
33
    const MAX_ADJUSTED_TIMESTAMP = 2199023255551;
34
35
    /**
36
     * Hexdec lookup.
37
     * 
38
     * @staticvar array
39
     */
40
    private static $hexdec = array(
41
        '0' => 0,
42
        '1' => 1,
43
        '2' => 2,
44
        '3' => 3,
45
        '4' => 4,
46
        '5' => 5,
47
        '6' => 6,
48
        '7' => 7,
49
        '8' => 8,
50
        '9' => 9,
51
        'a' => 10,
52
        'b' => 11,
53
        'c' => 12,
54
        'd' => 13,
55
        'e' => 14,
56
        'f' => 15,
57
    );
58
59
    /**
60
     * Timer.
61
     * 
62
     * @var TimerInterface
63
     */
64
    private $timer;
65
66
    /**
67
     * Configured machine ID - 10 bits (dec 0 -> 1023).
68
     *
69
     * @var int
70
     */
71
    private $machine;
72
73
    /**
74
     * Epoch - in UTC millisecond timestamp.
75
     *
76
     * @var int
77
     */
78
    private $epoch = 1325376000000;
79
80
    /**
81
     * Sequence number - 12 bits, we auto-increment for same-millisecond collisions.
82
     *
83
     * @var int
84
     */
85
    private $sequence = 1;
86
87
    /**
88
     * The most recent millisecond time window encountered.
89
     *
90
     * @var int
91
     */
92
    private $lastTime = null;
93
94
    /**
95
     * Constructor.
96
     * 
97
     * @param @inject ConfigInterface $config
98
     * @param @inject TimerInterface  $timer
99
     */
100 21
    public function __construct(ConfigInterface $config, TimerInterface $timer)
101
    {
102 21
        $this->machine = $config->getMachine();
103 21
        if (!is_int($this->machine) || $this->machine < 0 || $this->machine > 1023) {
104 4
            throw new InvalidArgumentException(
105
            'Machine identifier invalid -- must be 10 bit integer (0 to 1023)'
106 4
            );
107
        }
108 17
        $this->timer = $timer;
109 17
    }
110
111
    /**
112
     * Generate ID.
113
     *
114
     * @return string A 64 bit integer as a string of numbers (so we can deal
115
     *                with this on 32 bit platforms) 
116
     */
117 15
    public function generate()
118
    {
119 15
        $t = (int) floor($this->timer->getUnixTimestamp() - $this->epoch);
120 15
        if ($t !== $this->lastTime) {
121 15
            $this->generateWithDifferentTime($t);
122 13
        } else {
123 7
            $this->generateWithSameTime();
124
        }
125
126 13
        if ($this->is32Bit()) {
127 1
            return $this->mintId32($t, $this->machine, $this->sequence);
128
        } else {
129 12
            return $this->mintId64($t, $this->machine, $this->sequence);
130
        }
131
    }
132
    
133
    /**
134
     * Return true, if we are on 32 bit platform.
135
     * 
136
     * @return boolean
137
     */
138 12
    protected function is32Bit()
139
    {
140 12
        return (PHP_INT_SIZE === 4);
141
    }
142
143
    /**
144
     * Generate new ID with different time.
145
     * 
146
     * @param integer $t
147
     * @throws UnexpectedValueException
148
     * @throws OverflowException
149
     */
150 15
    private function generateWithDifferentTime($t)
151
    {
152 15
        if ($t < $this->lastTime) {
153 1
            throw new UnexpectedValueException(
154
            'Time moved backwards. We cannot generate IDs for '
155 1
            . ($this->lastTime - $t) . ' milliseconds'
156 1
            );
157 15
        } elseif ($t < 0) {
158 1
            throw new UnexpectedValueException(
159
            'Time is currently set before our epoch - unable '
160 1
            . 'to generate IDs for ' . (-$t) . ' milliseconds'
161 1
            );
162 14
        } elseif ($t > self::MAX_ADJUSTED_TIMESTAMP) {
163 1
            throw new OverflowException(
164
            'Timestamp overflow (past end of lifespan) - unable to generate any more IDs'
165 1
            );
166
        }
167 13
        $this->sequence = 0;
168 13
        $this->lastTime = $t;
169 13
    }
170
171
    /**
172
     * Generate new ID with same time.
173
     * 
174
     * @throws OverflowException
175
     */
176 7
    private function generateWithSameTime()
177
    {
178 7
        ++$this->sequence;
179 7
        if ($this->sequence > 4095) {
180 1
            throw new OverflowException(
181
            'Sequence overflow (too many IDs generated) - unable to generate IDs for 1 milliseconds'
182 1
            );
183
        }
184 7
    }
185
186
    /**
187
     * Get stats.
188
     *
189
     * @return GeneratorStatus
190
     */
191 1
    public function status()
192
    {
193 1
        return new GeneratorStatus($this->machine, $this->lastTime,
194 1
            $this->sequence, (PHP_INT_SIZE === 4));
195
    }
196
197 1
    private function mintId32($timestamp, $machine, $sequence)
198
    {
199 1
        $hi = (int) ($timestamp / pow(2, 10));
200 1
        $lo = (int) ($timestamp * pow(2, 22));
201
202
        // stick in the machine + sequence to the low bit
203 1
        $lo = $lo | ($machine << 12) | $sequence;
204
205
        // reconstruct into a string of numbers
206 1
        $hex = pack('N2', $hi, $lo);
207 1
        $unpacked = unpack('H*', $hex);
208 1
        $value = $this->hexdec($unpacked[1]);
209
210 1
        return (string) $value;
211
    }
212
213 12
    private function mintId64($timestamp, $machine, $sequence)
214
    {
215 12
        $timestamp = (int) $timestamp;
216 12
        $value = ($timestamp << 22) | ($machine << 12) | $sequence;
217
218 12
        return (string) $value;
219
    }
220
221 1
    private function hexdec($hex)
222
    {
223 1
        $dec = 0;
224 1
        for ($i = strlen($hex) - 1, $e = 1; $i >= 0; $i--, $e = bcmul($e, 16)) {
225 1
            $factor = self::$hexdec[$hex[$i]];
226 1
            $dec = bcadd($dec, bcmul($factor, $e));
227 1
        }
228
229 1
        return $dec;
230
    }
231
232
}
233