isValidBase62($value)) { throw new InvalidArgumentException('KSUID contains invalid characters'); } $this->value = $value; $this->parseComponents(); } /** * Create from raw bytes */ public static function fromBytes(string $bytes): self { if (strlen($bytes) !== self::TOTAL_BYTES) { throw new InvalidArgumentException('KSUID bytes must be exactly ' . self::TOTAL_BYTES . ' bytes'); } return new self(self::encodeBase62($bytes)); } /** * Create from string */ public static function fromString(string $value): self { return new self($value); } /** * Create from timestamp and payload */ public static function fromTimestampAndPayload(int $timestamp, string $payload): self { if (strlen($payload) !== self::PAYLOAD_BYTES) { throw new InvalidArgumentException('Payload must be exactly ' . self::PAYLOAD_BYTES . ' bytes'); } // Convert to KSUID timestamp (subtract epoch) $ksuidTimestamp = $timestamp - self::EPOCH; if ($ksuidTimestamp < 0) { throw new InvalidArgumentException('Timestamp cannot be before KSUID epoch'); } if ($ksuidTimestamp > 0xFFFFFFFF) { throw new InvalidArgumentException('Timestamp overflow'); } // Pack timestamp as big-endian 32-bit unsigned integer $timestampBytes = pack('N', $ksuidTimestamp); $bytes = $timestampBytes . $payload; return self::fromBytes($bytes); } /** * Get the string representation */ public function toString(): string { return $this->value; } /** * Get the string value */ public function getValue(): string { return $this->value; } /** * Magic method for string conversion */ public function __toString(): string { return $this->value; } /** * Get the timestamp component (Unix timestamp) */ public function getTimestamp(): int { return $this->timestamp; } /** * Get the timestamp as DateTime */ public function getDateTime(): DateTimeImmutable { return DateTimeImmutable::createFromFormat('U', (string)$this->timestamp); } /** * Get the raw payload bytes */ public function getPayload(): string { return $this->payload; } /** * Get raw bytes representation */ public function getBytes(): string { return self::decodeBase62($this->value); } /** * Check equality with another KSUID */ public function equals(self $other): bool { return $this->value === $other->value; } /** * Compare with another KSUID for sorting */ public function compare(self $other): int { return strcmp($this->value, $other->value); } /** * Check if this KSUID is older than another */ public function isOlderThan(self $other): bool { return $this->timestamp < $other->timestamp; } /** * Check if this KSUID is newer than another */ public function isNewerThan(self $other): bool { return $this->timestamp > $other->timestamp; } /** * Get age in seconds */ public function getAgeInSeconds(): int { return time() - $this->timestamp; } /** * Validate Base62 characters */ private function isValidBase62(string $value): bool { $pattern = '/^[' . preg_quote(self::ALPHABET, '/') . ']+$/'; return preg_match($pattern, $value) === 1; } /** * Parse timestamp and payload components from the value */ private function parseComponents(): void { $bytes = self::decodeBase62($this->value); // Extract timestamp (first 4 bytes) $timestampBytes = substr($bytes, 0, self::TIMESTAMP_BYTES); $ksuidTimestamp = unpack('N', $timestampBytes)[1]; $this->timestamp = $ksuidTimestamp + self::EPOCH; // Extract payload (remaining 16 bytes) $this->payload = substr($bytes, self::TIMESTAMP_BYTES); } /** * Encode bytes to Base62 */ private static function encodeBase62(string $bytes): string { // Convert bytes to big integer using BcMath\Number $num = new Number(0); $base = new Number(1); $base256 = new Number(256); // Process each byte from right to left for ($i = strlen($bytes) - 1; $i >= 0; $i--) { $byte = new Number(ord($bytes[$i])); $num = $num->add($byte->mul($base)); $base = $base->mul($base256); } // Convert to Base62 $alphabet = self::ALPHABET; $encoded = ''; $base62 = new Number(62); $zero = new Number(0); if ($num->compare($zero) === 0) { return str_repeat($alphabet[0], self::ENCODED_LENGTH); } while ($num->compare($zero) > 0) { [$quotient, $remainder] = $num->divmod($base62); $encoded = $alphabet[(int)$remainder->__toString()] . $encoded; $num = $quotient; } // Pad with leading zeros if necessary return str_pad($encoded, self::ENCODED_LENGTH, $alphabet[0], STR_PAD_LEFT); } /** * Decode Base62 to bytes */ private static function decodeBase62(string $encoded): string { $alphabet = self::ALPHABET; $base62 = new Number(62); $num = new Number(0); $multiplier = new Number(1); // Convert from Base62 to big integer for ($i = strlen($encoded) - 1; $i >= 0; $i--) { $char = $encoded[$i]; $value = strpos($alphabet, $char); if ($value === false) { throw new InvalidArgumentException('Invalid Base62 character: ' . $char); } $num = $num->add(new Number($value)->mul($multiplier)); $multiplier = $multiplier->mul($base62); } // Convert big integer to bytes $bytes = ''; $base256 = new Number(256); $zero = new Number(0); while ($num->compare($zero) > 0) { [$quotient, $remainder] = $num->divmod($base256); $bytes = chr((int)$remainder->__toString()) . $bytes; $num = $quotient; } // Pad with leading zero bytes if necessary return str_pad($bytes, self::TOTAL_BYTES, "\0", STR_PAD_LEFT); } }