Created
March 18, 2018 00:17
-
-
Save derrekbertrand/29e35edcf18e8d704691c80513c5ecdb to your computer and use it in GitHub Desktop.
This file contains hidden or 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
<?php | |
/* | |
* Copyright 2017-2018 Derrek Bertrand <[email protected]> | |
* Copyright 2018 CollabCorp Group | |
* | |
* License: MIT | |
*/ | |
use Exception; | |
/** | |
* K-Sortable Unique ID Class | |
* | |
* Requires: | |
* - A 64 bit PHP runtime | |
* - Probably a little endian system? Haven't tested. | |
* - PHP 7+ | |
* | |
* When to use this: | |
* - You need collision resistant IDs generated by multiple application servers. | |
* (auto-incrementing integer) | |
* - You don't need your ID to tell you where it was generated. (UUID 1,2) | |
* - You're not okay wasting 48 bits of entropy on a MAC address. (UUID 1,2) | |
* - You don't need to be able to map a namespace and name to your IDs. | |
* (UUID 3,5) | |
* - You don't use SHA1 or MD5 to obfuscate anything, because you're not from | |
* 2005. (UUID 3, 5) | |
* - You don't need an RFC spec to generate random bytes. (UUID 4) | |
* - You don't want your database index cluttered with junk or having bad lookup | |
* times. (UUID 1,2,3,4,5) | |
* | |
* When NOT to use this: | |
* - Do not use this to generate anything "security" related whatsoever. | |
* - Do not use this for cases where "when" something was generated is a secret. | |
* The timestamp is right there in the ID. | |
* - Do not allow clients to generate Ksuids without restriction. Either use a | |
* different kind of ID, or allow them to use ephemeral IDs. | |
* | |
* This generates unique IDs that are k-sortable within 1 second. This means | |
* that once inserted into the database they are partially sortable, making them | |
* more efficient as indexes. They take up 128 bits like a binary UUID, and have | |
* several representation flavors. | |
* | |
* There are 40 bits of an unsigned 64 bit UNIX timestamp, meaning IDs can be | |
* generated for another 30,000 years. | |
* | |
* The other 88 bits are cryptographically random. The number of IDs generated | |
* each second to have a 50% probability of collisions is about 2.07*10^13. This | |
* is about 20 trillion, 700 Billion insertions a second. | |
* | |
* In order to have one in a billion chance of collision, 786 million IDs must | |
* be generated in less than one second. This is sufficient for many use cases | |
* while remaining K-sortable. | |
*/ | |
class Ksuid | |
{ | |
protected $value = null; | |
public function __construct($value = null) | |
{ | |
// This implementation only runs on a 64 bit system! | |
// Although, it is set up to at least have the possibility of being | |
// ported to other architectures. | |
if (PHP_INT_SIZE !== 8) { | |
throw new Exception('Ksuid only runs on 64 bit systems!'); | |
} | |
// set the value if provided, otherwise do nothing | |
if ($value !== null) { | |
$this->setValue($value); | |
} | |
} | |
/** | |
* Determines if this Ksuid is equal to another. | |
* | |
* @param Ksuid $ksuid | |
* @return boolean | |
*/ | |
public function isEqualToKsuid(Ksuid $ksuid) | |
{ | |
return !strcmp($this->getValue(), $ksuid->getValue()); | |
} | |
/** | |
* Returns the value as a base 64 string. IMPORTANT: see notes. | |
* | |
* This representation is NOT K-sortable, and thus not suitable for | |
* persistence. | |
* | |
* @return string | |
*/ | |
public function getBase64() | |
{ | |
return base64_encode($this->getValue()); | |
} | |
/** | |
* Returns the value as a hexadecimal string. | |
* | |
* @return string | |
*/ | |
public function getHex() | |
{ | |
return bin2hex($this->getValue()); | |
} | |
/** | |
* Returns the value as a UUIDv4-style string. IMPORTANT: see notes. | |
* | |
* This representation is structured like a UUID, but it is NOT a UUID. It | |
* claims to be a UUID v4, because that's the only version where we know | |
* an external system will not try to derive information from it. This | |
* representation is provided to assist with compatibility, but you should | |
* understand the implications of using this format. | |
* | |
* You do not gain the benefits of UUID v4 just by outputting this format. | |
* The timestamp is still in the output. You lose entropy by using this | |
* format. Six bits are overwritten, so their randomness is gone. | |
* Additionally, this means you should use caution when comparing values | |
* to the output of this method. | |
* | |
* @return string | |
*/ | |
public function getUuidV4() | |
{ | |
$pack = unpack('H8a/H4b/nc/nd/H12e', $this->getValue()); | |
$pack['c'] = dechex($pack['c'] & 0x0fff | 0x4000); //UUID version | |
$pack['d'] = dechex($pack['d'] & 0x3fff | 0x8000); //reserved bits | |
return implode('-', $pack); | |
} | |
/** | |
* Returns the value as a binary string; generates one if not set. | |
* | |
* @return string | |
*/ | |
public function getValue() | |
{ | |
return $this->value ?? $this->setValue()->value; | |
} | |
/** | |
* Set or generate a new Ksuid. | |
* | |
* @param mixed $value | |
* @return $this | |
*/ | |
public function setValue($value = null) | |
{ | |
if ($value === null) { | |
// no value is provided, generate one | |
$this->value = pack('JXXX', time()<<24).random_bytes(11); | |
} elseif ($value instanceof Ksuid) { | |
// set from another Ksuid | |
$this->value = $value->getValue(); | |
} elseif (is_string($value) && strlen($value) === 32 && preg_match('/[^0-9A-Fa-f]/', $value) === 0) { | |
// a hexadecimal value is provided | |
$this->value = hex2bin($value); | |
} else { | |
throw new Exception('Ksuid only accepts hexadecimal strings when setting value.'); | |
} | |
return $this; | |
} | |
/** | |
* Returns the value as a string. | |
* | |
* @return string | |
*/ | |
public function __toString() | |
{ | |
return $this->getValue(); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment