-
-
Save yoghi/3703e5e1392c0dc16927 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/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