Skip to content

Instantly share code, notes, and snippets.

@yoghi
Forked from andrewmackrodt/distributed-uuid
Last active September 21, 2015 19:39
Show Gist options
  • Save yoghi/3703e5e1392c0dc16927 to your computer and use it in GitHub Desktop.
Save yoghi/3703e5e1392c0dc16927 to your computer and use it in GitHub Desktop.
#!/usr/bin/env php
<?php
/**
* Generates a naturally sortable UUID in a distributed environment.
* UUID's are 120-bit represented in base62 as 20 in length and are composed of:
* - a time stamp in microseconds (from 1970-01-01 until 3180-01-01; ~1,200 years if using a custom epoch)
* - the machine's MAC address
* - the running process' id
*/
class UniqueId {
private static $valid_digits = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
private $epoch = '1970-01-01';
private $epoch_us = 0;
private $mac_address;
private $mac_address_bits;
private $output_base = 62;
private $pid;
private $pid_bits;
public function __construct($mac_address = null, $epoch = '1970-01-01', $pid = null) {
$this->setMacAddress($mac_address);
$this->setEpoch($epoch);
$this->setProcessId($pid);
}
public function decode($id) {
$id = strtr(ltrim($id, 0),
'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ',
'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz');
$id = self::bc_base_convert($id, $this->output_base, 2);
$pid = self::bc_base_convert(substr($id, -16), 2, 10);
$mac_address = self::bc_base_convert(substr($id, -64, 48), 2, 16);
$time_us = self::bc_base_convert(substr($id, 0, strlen($id) - 64), 2, 10);
if ($this->epoch_us) {
$time_us = bcadd($time_us, $this->epoch_us);
}
return array(
'time_us' => $time_us,
'mac_address' => preg_replace('/^(.{2})(.{2})(.{2})(.{2})(.{2})(.{2})$/', '$1:$2:$3:$4:$5:$6', $mac_address),
'pid' => $pid,
'datetime' => (new DateTime('@' . substr($time_us, 0, -6)))->format('c') // UTC
);
}
public function get() {
// use DateTime::getTimestamp for unix timestamp as microtime is unaffected by php_timecop.so
$time_us = (new DateTime('now UTC'))->getTimestamp()
. str_pad(substr(microtime(), 2, 6), 6, 0, STR_PAD_LEFT);
if ($this->epoch_us) {
$time_us = bcsub($time_us, $this->epoch_us);
if ($time_us < 0) {
if ($time_us < -1000000) {
throw new \Exception('Invalid epoch');
}
// only occurs if generating a timestamp exactly at the epoch
$time_us = ltrim($time_us, '-');
}
}
$time_us_bits = static::bc_base_convert($time_us, 10, 2);
$id = static::bc_base_convert(
$time_us_bits
. $this->mac_address_bits
. $this->pid_bits,
2, $this->output_base);
$id = strtr($id, // inverts the case of the id for natural sequential ordering
'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz',
'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ');
$id = str_pad($id, 20, 0, STR_PAD_LEFT);
return $id;
}
public function setMacAddress($mac_address) {
$findHwAddr = function ($stdout) {
if (preg_match('/HWaddr ([a-f0-9:]+)/', $stdout, $match)) {
return $match[1];
}
return false;
};
if ($mac_address) {
if (strlen($mac_address) < 12) {
$stdout = shell_exec('ifconfig ' . escapeshellarg($mac_address) . ' 2>/dev/null');
$mac_address = $findHwAddr($stdout);
}
} else {
$stdout = shell_exec('ifconfig 2>/dev/null');
$mac_address = $findHwAddr($stdout);
}
$mac_address = strtolower(str_replace(':', '', $mac_address));
if (!preg_match('/^[a-f0-9]{12}$/', $mac_address)) {
throw new \Exception('Invalid MAC address');
}
$this->mac_address = preg_replace('/^(.{2})(.{2})(.{2})(.{2})(.{2})(.{2})$/', '$1:$2:$3:$4:$5:$6', $mac_address);
$this->mac_address_bits = self::bc_base_convert_pad($mac_address, 16, 48);
}
public function getMacAddress() {
return $this->mac_address;
}
public function setEpoch($epoch = null) {
if ($epoch) {
if (is_numeric($epoch)) {
$epoch = "@$epoch";
}
} else {
$epoch = '1970-01-01';
}
$epoch = strtotime("$epoch UTC");
$this->epoch = $epoch;
$this->epoch_us = ltrim("{$epoch}000000", 0) ?: 0;
}
public function getEpoch() {
return $this->epoch;
}
public function setProcessId($pid = null) {
if (!$pid) {
$pid = getmypid();
}
$this->pid = $pid;
$this->pid_bits = self::bc_base_convert_pad($pid, 10, 16);
}
public function getProcessId() {
return $this->pid;
}
/**
* Convert an arbitrary-length number between arbitrary bases.
*
* @author Ulrich Floer
* @email [email protected]
* @link http://www.technischedaten.de/pmwiki2/pmwiki.php?n=Php.BaseConvert
*
* @param string|int $value
* @param int $from_base
* @param int $to_base
*
* @return string
*/
private static function bc_base_convert($value, $from_base, $to_base) {
if (min($from_base, $to_base) < 2) {
trigger_error('Bad Format min: 2', E_USER_ERROR);
}
if (max($from_base, $to_base) > strlen(self::$valid_digits)) {
trigger_error('Bad Format max: ' . strlen(self::$valid_digits), E_USER_ERROR);
}
$dezi = '0';
$level = 0;
$result = '';
$value = trim(strval($value), "\r\n\t +");
$sign = ('-' === $value[0]) ? '-' : '';
$value = ltrim($value, "-0");
for ($i = 0, $len = strlen($value); $i < $len; $i++) {
$bits = strpos(self::$valid_digits, $value[$len - 1 - $i]);
if (false === $bits) {
trigger_error('Bad Char in input 1', E_USER_ERROR);
}
$dezi = bcadd($dezi, bcmul(bcpow($from_base, $i), $bits));
}
if ($to_base == 10) {
return $sign . $dezi; // Shortcut for base 10
}
while (1 !== bccomp(bcpow($to_base, $level++), $dezi));
for ($i = $level - 2; $i >= 0; $i--) {
$factor = bcpow($to_base, $i);
$zahl = bcdiv($dezi, $factor, 0);
$dezi = bcmod($dezi, $factor);
$result .= self::$valid_digits[$zahl];
}
$result = empty($result) ? '0' : $result;
return $sign . $result;
}
private static function bc_base_convert_pad($value, $from_base, $length) {
$value = self::bc_base_convert($value, $from_base, 2);
$value = str_pad($value, $length, 0, STR_PAD_LEFT);
return $value;
}
}
ini_set('date.timezone', 'Etc/GMT-8');
$uuidgen = new UniqueId();
$start = microtime(true);
$i = 0;
while (microtime(true) - $start < 1) {
$uuidgen->get();
$i++;
}
echo "$i id/s\n\n";
$epoch = '1970-01-01';
$uuidgen = new UniqueId(null, $epoch);
$startTime = strtotime($epoch);
$endTime = strtotime('+1210 years', $startTime);
$resultSet = array();
for ($i = $startTime, $j = null; $i <= $endTime; $i = strtotime('+1 year', $i)) {
$date = date('Y-m-d H:i', $i);
timecop_travel($i);
$id = $uuidgen->get();
$resultSet[$id] = $date;
}
ksort($resultSet);
$lastDate = null;
foreach ($resultSet as $k => $v) {
echo "$k\t$v\n";
if ($lastDate && $lastDate > $v) {
echo "\n\nDate order error\n";
exit;
}
$lastDate = $v;
}
echo "\n" . print_r($uuidgen->decode($k), true);
echo "\n";
// if using 36 character base:
//timecop_return();
//
//for ($i = 0; $i < 100; $i++) {
// echo preg_replace('/(.{9})(.{4})(.{4})(.{6})/', '{$1-$2-$3-$4}', strtolower($uuidgen->get())) . "\n";
//}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment