| 1: | <?php
|
| 2: | |
| 3: | |
| 4: | |
| 5: | |
| 6: | |
| 7: | |
| 8: | |
| 9: | |
| 10: |
|
| 11: |
|
| 12: | namespace Xmf;
|
| 13: |
|
| 14: | |
| 15: | |
| 16: | |
| 17: | |
| 18: | |
| 19: | |
| 20: | |
| 21: | |
| 22: |
|
| 23: | class Ulid
|
| 24: | {
|
| 25: | const ENCODING_CHARS = '0123456789ABCDEFGHJKMNPQRSTVWXYZ';
|
| 26: | const ENCODING_LENGTH = 32;
|
| 27: |
|
| 28: | |
| 29: | |
| 30: | |
| 31: | |
| 32: |
|
| 33: | public static function generate(bool $upperCase = true): string
|
| 34: | {
|
| 35: | $time = self::microtimeToUlidTime(\microtime(true));
|
| 36: | $timeChars = self::encodeTime($time);
|
| 37: | $randChars = self::encodeRandomness();
|
| 38: | $ulid = $timeChars . $randChars;
|
| 39: |
|
| 40: | $ulid = $upperCase ? \strtoupper($ulid) : \strtolower($ulid);
|
| 41: |
|
| 42: | return $ulid;
|
| 43: | }
|
| 44: |
|
| 45: | |
| 46: | |
| 47: | |
| 48: | |
| 49: |
|
| 50: | public static function encodeTime(int $time): string
|
| 51: | {
|
| 52: | $encodingCharsArray = str_split(self::ENCODING_CHARS);
|
| 53: | $timeChars = '';
|
| 54: | for ($i = 0; $i < 10; $i++) {
|
| 55: | $mod = \floor($time % self::ENCODING_LENGTH);
|
| 56: | $timeChars = $encodingCharsArray[$mod] . $timeChars;
|
| 57: | $time = (int)(($time - $mod) / self::ENCODING_LENGTH);
|
| 58: | }
|
| 59: | return $timeChars;
|
| 60: | }
|
| 61: |
|
| 62: | public static function encodeRandomness(): string
|
| 63: | {
|
| 64: | $encodingCharsArray = str_split(self::ENCODING_CHARS);
|
| 65: | $randomBytes = \random_bytes(10);
|
| 66: |
|
| 67: | if (false === $randomBytes) {
|
| 68: | throw new \RuntimeException('Failed to generate random bytes');
|
| 69: | }
|
| 70: |
|
| 71: | $randChars = '';
|
| 72: | for ($i = 0; $i < 16; $i++) {
|
| 73: | $randValue = \ord($randomBytes[$i % 10]);
|
| 74: | if (0 === $i % 2) {
|
| 75: | $randValue >>= 3;
|
| 76: | } else {
|
| 77: | $randValue &= 31;
|
| 78: | }
|
| 79: | $randChars .= $encodingCharsArray[$randValue];
|
| 80: | }
|
| 81: | return $randChars;
|
| 82: | }
|
| 83: |
|
| 84: | |
| 85: | |
| 86: | |
| 87: | |
| 88: |
|
| 89: | public static function decode(string $ulid): array
|
| 90: | {
|
| 91: | if (!self::isValid($ulid)) {
|
| 92: | throw new \InvalidArgumentException('Invalid ULID string');
|
| 93: | }
|
| 94: |
|
| 95: | $time = self::decodeTime($ulid);
|
| 96: | $rand = self::decodeRandomness($ulid);
|
| 97: |
|
| 98: | return [
|
| 99: | 'time' => $time,
|
| 100: | 'rand' => $rand,
|
| 101: | ];
|
| 102: | }
|
| 103: |
|
| 104: | |
| 105: | |
| 106: | |
| 107: | |
| 108: |
|
| 109: | public static function decodeTime(string $ulid): int
|
| 110: | {
|
| 111: |
|
| 112: |
|
| 113: |
|
| 114: | if (!self::isValid($ulid)) {
|
| 115: | throw new \InvalidArgumentException('Invalid ULID string');
|
| 116: | }
|
| 117: |
|
| 118: | $time = 0;
|
| 119: | for ($i = 0; $i < 10; $i++) {
|
| 120: | $char = $ulid[$i];
|
| 121: | $value = \strpos(self::ENCODING_CHARS, $char);
|
| 122: | $exponent = 9 - $i;
|
| 123: | $time += $value * \bcpow((string)self::ENCODING_LENGTH, (string)$exponent);
|
| 124: | }
|
| 125: |
|
| 126: | return $time;
|
| 127: | }
|
| 128: |
|
| 129: | |
| 130: | |
| 131: | |
| 132: | |
| 133: |
|
| 134: | public static function decodeRandomness(string $ulid): int
|
| 135: | {
|
| 136: | if (26 !== strlen($ulid)) {
|
| 137: | throw new \InvalidArgumentException('Invalid ULID length');
|
| 138: | }
|
| 139: |
|
| 140: | $rand = 0;
|
| 141: | for ($i = 10; $i < 26; $i++) {
|
| 142: | $char = $ulid[$i];
|
| 143: | $value = \strpos(self::ENCODING_CHARS, $char);
|
| 144: |
|
| 145: |
|
| 146: | if ($value < 0 || $value >= self::ENCODING_LENGTH) {
|
| 147: | throw new \InvalidArgumentException('Invalid ULID random value');
|
| 148: | }
|
| 149: | $exponent = 15 - $i;
|
| 150: | $rand += $value * \bcpow((string)self::ENCODING_LENGTH, (string)$exponent);
|
| 151: | }
|
| 152: |
|
| 153: | return $rand;
|
| 154: | }
|
| 155: |
|
| 156: | |
| 157: | |
| 158: | |
| 159: | |
| 160: |
|
| 161: | public static function isValid(string $ulid): bool
|
| 162: | {
|
| 163: |
|
| 164: | if (26 !== strlen($ulid)) {
|
| 165: | return false;
|
| 166: | }
|
| 167: |
|
| 168: |
|
| 169: | try {
|
| 170: | self::decodeRandomness($ulid);
|
| 171: | } catch (\InvalidArgumentException $e) {
|
| 172: | return false;
|
| 173: | }
|
| 174: |
|
| 175: | return true;
|
| 176: | }
|
| 177: |
|
| 178: | |
| 179: | |
| 180: | |
| 181: | |
| 182: |
|
| 183: | public static function microtimeToUlidTime(float $microtime): int
|
| 184: | {
|
| 185: | $timestamp = $microtime * 1000000;
|
| 186: | $unixEpoch = 946684800000000;
|
| 187: |
|
| 188: | return (int)($timestamp - $unixEpoch);
|
| 189: | }
|
| 190: | }
|
| 191: |
|
| 192: |
|
| 193: |
|
| 194: | |