| 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: |  |