Skip to content

Instantly share code, notes, and snippets.

@pablomayobre
Forked from jbroadway/PushId.php
Last active August 14, 2021 22:51
Show Gist options
  • Save pablomayobre/c2923126d44907def1c8ff469cd69433 to your computer and use it in GitHub Desktop.
Save pablomayobre/c2923126d44907def1c8ff469cd69433 to your computer and use it in GitHub Desktop.
Firebase Push ID, in PHP
<?php
/**
* Fancy ID generator that creates 20-character string identifiers with the following properties:
*
* 1. They're based on timestamp so that they sort *after* any existing ids.
* 2. They contain 72-bits of random data after the timestamp so that IDs won't collide with other clients' IDs.
* 3. They sort *lexicographically* (so the timestamp is converted to characters that will sort properly).
* 4. They're monotonically increasing. Even if you generate more than one in the same timestamp, the
* latter ones will sort after the former ones. We do this by using the previous random bits
* but "incrementing" them by 1 (only in the case of a timestamp collision).
*/
class PushId
{
/**
* Modeled after base64 web-safe chars, but ordered by ASCII.
*
* @var string
*/
const PUSH_CHARS = '-0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ_abcdefghijklmnopqrstuvwxyz';
/**
* Timestamp of last push, used to prevent local collisions if you push twice in one ms.
*
* @var int
*/
private $lastPushTime = 0;
/**
* We generate 72-bits of randomness which get turned into 12 characters and appended to the
* timestamp to prevent collisions with other clients. We store the last characters we
* generated because in the event of a collision, we'll use those same characters except
* "incremented" by one.
*
* @var array
*/
private $lastRandChars = [];
public function __construct() {
$this->lastRandChars = [];
$this->lastPushTime = 0;
}
public function generate(): string
{
$now = microtime(true) * 1000;
$isDuplicateTime = ($now === $this->$lastPushTime);
$this->$lastPushTime = $now;
$timeStampChars = new SplFixedArray(8);
for ($i = 7; $i >= 0; $i--) {
$timeStampChars[$i] = substr(self::PUSH_CHARS, (int) $now % 64, 1);
$now = floor($now / 64);
}
static::assert((int) $now === 0, 'We should have converted the entire timestamp.');
$id = implode('', $timeStampChars->toArray());
if (!$isDuplicateTime) {
for ($i = 0; $i < 12; $i++) {
$this->$lastRandChars[$i] = random_int(0, 63);
}
} else {
// If the timestamp hasn't changed since last push, use the same random number, except incremented by 1.
for ($i = 11; $i >= 0 && $this->$lastRandChars[$i] === 63; $i--) {
$this->$lastRandChars[$i] = 0;
}
$this->$lastRandChars[$i]++;
}
for ($i = 0; strlen($id) < 20; $i++) {
$id .= substr(self::PUSH_CHARS, $this->$lastRandChars[$i], 1);
}
static::assert(strlen($id) === 20, 'Length should be 20.');
return $id;
}
/**
* @param bool $condition
* @param string $message
*/
private static function assert($condition, $message = '')
{
if ($condition !== true) {
throw new RuntimeException($message);
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment