Skip to content

Instantly share code, notes, and snippets.

@derrekbertrand
Created March 18, 2018 00:17
Show Gist options
  • Save derrekbertrand/29e35edcf18e8d704691c80513c5ecdb to your computer and use it in GitHub Desktop.
Save derrekbertrand/29e35edcf18e8d704691c80513c5ecdb to your computer and use it in GitHub Desktop.
<?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