Files
michaelschiemer/src/Framework/Ksuid/KsuidGenerator.php
Michael Schiemer 55a330b223 Enable Discovery debug logging for production troubleshooting
- Add DISCOVERY_LOG_LEVEL=debug
- Add DISCOVERY_SHOW_PROGRESS=true
- Temporary changes for debugging InitializerProcessor fixes on production
2025-08-11 20:13:26 +02:00

209 lines
5.5 KiB
PHP

<?php
declare(strict_types=1);
namespace App\Framework\Ksuid;
use App\Framework\Random\RandomGenerator;
use DateTimeImmutable;
use InvalidArgumentException;
/**
* KSUID Generator Service
*
* Generates K-Sortable Unique Identifiers with timestamp ordering.
*/
final readonly class KsuidGenerator
{
public function __construct(
private RandomGenerator $randomGenerator
) {
}
/**
* Generate a new KSUID with current timestamp
*/
public function generate(): Ksuid
{
return $this->generateAt(time());
}
/**
* Generate a KSUID at a specific Unix timestamp
*/
public function generateAt(int $timestamp): Ksuid
{
if ($timestamp < Ksuid::EPOCH) {
throw new InvalidArgumentException('Timestamp cannot be before KSUID epoch (2014-05-13)');
}
// Generate 16 random bytes for payload
$payload = $this->randomGenerator->bytes(Ksuid::PAYLOAD_BYTES);
return Ksuid::fromTimestampAndPayload($timestamp, $payload);
}
/**
* Generate a KSUID at a specific DateTime
*/
public function generateAtDateTime(DateTimeImmutable $dateTime): Ksuid
{
return $this->generateAt($dateTime->getTimestamp());
}
/**
* Generate a KSUID for the past (useful for testing)
*/
public function generateInPast(int $secondsAgo): Ksuid
{
$timestamp = time() - $secondsAgo;
return $this->generateAt($timestamp);
}
/**
* Generate a batch of KSUIDs with the same timestamp
*/
public function generateBatch(int $count, ?int $timestamp = null): array
{
if ($count <= 0) {
throw new InvalidArgumentException('Count must be positive');
}
if ($count > 10000) {
throw new InvalidArgumentException('Batch size cannot exceed 10000');
}
$timestamp ??= time();
$ksuids = [];
$used = [];
while (count($ksuids) < $count) {
$payload = $this->randomGenerator->bytes(Ksuid::PAYLOAD_BYTES);
$payloadHex = bin2hex($payload);
// Ensure uniqueness within batch
if (! isset($used[$payloadHex])) {
$ksuids[] = Ksuid::fromTimestampAndPayload($timestamp, $payload);
$used[$payloadHex] = true;
}
}
return $ksuids;
}
/**
* Generate a sequence of KSUIDs with incrementing timestamps
*/
public function generateSequence(int $count, int $intervalSeconds = 1): array
{
if ($count <= 0) {
throw new InvalidArgumentException('Count must be positive');
}
if ($count > 1000) {
throw new InvalidArgumentException('Sequence size cannot exceed 1000');
}
if ($intervalSeconds < 0) {
throw new InvalidArgumentException('Interval must be non-negative');
}
$ksuids = [];
$timestamp = time();
for ($i = 0; $i < $count; $i++) {
$ksuids[] = $this->generateAt($timestamp + ($i * $intervalSeconds));
}
return $ksuids;
}
/**
* Generate a KSUID with specific prefix in payload (for testing/debugging)
*/
public function generateWithPrefix(string $prefix): Ksuid
{
$maxPrefixLength = Ksuid::PAYLOAD_BYTES - 1; // Reserve at least 1 byte for randomness
if (strlen($prefix) > $maxPrefixLength) {
throw new InvalidArgumentException("Prefix cannot exceed {$maxPrefixLength} bytes");
}
$remainingBytes = Ksuid::PAYLOAD_BYTES - strlen($prefix);
$randomSuffix = $this->randomGenerator->bytes($remainingBytes);
$payload = $prefix . $randomSuffix;
return Ksuid::fromTimestampAndPayload(time(), $payload);
}
/**
* Parse a KSUID string and validate it
*/
public function parse(string $ksuidString): Ksuid
{
return Ksuid::fromString($ksuidString);
}
/**
* Validate if a string is a valid KSUID
*/
public function isValid(string $value): bool
{
if (strlen($value) !== Ksuid::ENCODED_LENGTH) {
return false;
}
try {
Ksuid::fromString($value);
return true;
} catch (InvalidArgumentException) {
return false;
}
}
/**
* Get the minimum possible KSUID for a timestamp
*/
public function getMinForTimestamp(int $timestamp): Ksuid
{
$payload = str_repeat("\0", Ksuid::PAYLOAD_BYTES);
return Ksuid::fromTimestampAndPayload($timestamp, $payload);
}
/**
* Get the maximum possible KSUID for a timestamp
*/
public function getMaxForTimestamp(int $timestamp): Ksuid
{
$payload = str_repeat("\xFF", Ksuid::PAYLOAD_BYTES);
return Ksuid::fromTimestampAndPayload($timestamp, $payload);
}
/**@return array{min: \App\Framework\Ksuid\Ksuid, max: \App\Framework\Ksuid\Ksuid}
* Generate KSUIDs for a time range (useful for queries)
*/
public function generateTimeRange(int $startTimestamp, int $endTimestamp): array
{
if ($startTimestamp >= $endTimestamp) {
throw new InvalidArgumentException('Start timestamp must be before end timestamp');
}
return [
'min' => $this->getMinForTimestamp($startTimestamp),
'max' => $this->getMaxForTimestamp($endTimestamp),
];
}
/**
* Create factory method
*/
public static function create(RandomGenerator $randomGenerator): self
{
return new self($randomGenerator);
}
}