Completed
Push — master ( cd0397...35e616 )
by Tomasz
03:34
created

Generator::mintId64()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 7
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 1

Importance

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