refactor(console, id, config): Dialog mode in Console, consolidated id modul, added config support for ini directives

This commit is contained in:
2025-11-04 13:44:27 +01:00
parent 980714f656
commit bfce93ce77
110 changed files with 2828 additions and 774 deletions

View File

@@ -0,0 +1,31 @@
<?php
declare(strict_types=1);
namespace App\Framework\Id\Contracts;
/**
* Common interface for all ID generators
*/
interface IdGeneratorInterface
{
/**
* Generate a new ID
*
* @return IdInterface|string Returns either an IdInterface or string representation
*/
public function generate(): IdInterface|string;
/**
* Generate a batch of IDs
*
* @param int $count Number of IDs to generate
* @return array<int, IdInterface|string>
*/
public function generateBatch(int $count): array;
/**
* Validate if a string is a valid ID for this generator
*/
public function isValid(string $value): bool;
}

View File

@@ -0,0 +1,26 @@
<?php
declare(strict_types=1);
namespace App\Framework\Id\Contracts;
/**
* Common interface for all ID value objects
*/
interface IdInterface
{
/**
* Get the string representation of the ID
*/
public function toString(): string;
/**
* Get the string value
*/
public function getValue(): string;
/**
* Check equality with another ID
*/
public function equals(self $other): bool;
}

View File

@@ -0,0 +1,281 @@
<?php
declare(strict_types=1);
namespace App\Framework\Id\Cuid;
use App\Framework\Id\Contracts\IdInterface;
use DateTimeImmutable;
use InvalidArgumentException;
use Stringable;
/**
* Cuid Value Object
*
* Collision-resistant Unique Identifier
* - 25 characters total (c + timestamp + counter + fingerprint + random)
* - Base36 encoded (0-9, a-z) - case insensitive
* - Optimized for horizontal scaling and collision resistance
* - Always starts with 'c' for collision-resistant
*/
final readonly class Cuid implements IdInterface, Stringable
{
public const int LENGTH = 25;
public const string PREFIX = 'c';
// Base36 alphabet: 0-9, a-z (lowercase only)
public const string ALPHABET = '0123456789abcdefghijklmnopqrstuvwxyz';
// Component lengths
public const int TIMESTAMP_LENGTH = 8;
public const int COUNTER_LENGTH = 4;
public const int FINGERPRINT_LENGTH = 4;
public const int RANDOM_LENGTH = 8; // PREFIX(1) + TIMESTAMP(8) + COUNTER(4) + FINGERPRINT(4) + RANDOM(8) = 25
private string $value;
private int $timestamp;
private int $counter;
private string $fingerprint;
private string $random;
public function __construct(string $value)
{
if (empty($value)) {
throw new InvalidArgumentException('Cuid cannot be empty');
}
if (strlen($value) !== self::LENGTH) {
throw new InvalidArgumentException('Cuid must be exactly ' . self::LENGTH . ' characters long');
}
if (! str_starts_with($value, self::PREFIX)) {
throw new InvalidArgumentException('Cuid must start with "c"');
}
if (! $this->isValidBase36($value)) {
throw new InvalidArgumentException('Cuid contains invalid characters');
}
$this->value = $value;
$this->parseComponents();
}
/**
* Create from components
*/
public static function fromComponents(
int $timestamp,
int $counter,
string $fingerprint,
string $random
): self {
if (strlen($fingerprint) !== self::FINGERPRINT_LENGTH) {
throw new InvalidArgumentException('Fingerprint must be exactly ' . self::FINGERPRINT_LENGTH . ' characters');
}
if (strlen($random) !== self::RANDOM_LENGTH) {
throw new InvalidArgumentException('Random part must be exactly ' . self::RANDOM_LENGTH . ' characters');
}
// Convert components to Base36
$timestampBase36 = self::toBase36($timestamp, self::TIMESTAMP_LENGTH);
$counterBase36 = self::toBase36($counter, self::COUNTER_LENGTH);
$value = self::PREFIX . $timestampBase36 . $counterBase36 . $fingerprint . $random;
return new self($value);
}
/**
* Create from string
*/
public static function fromString(string $value): self
{
return new self($value);
}
/**
* 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 in milliseconds)
*/
public function getTimestamp(): int
{
return $this->timestamp;
}
/**
* Get the timestamp as DateTime
*/
public function getDateTime(): DateTimeImmutable
{
// Convert milliseconds to seconds
$seconds = intval($this->timestamp / 1000);
$microseconds = ($this->timestamp % 1000) * 1000;
return DateTimeImmutable::createFromFormat('U.u', sprintf('%d.%06d', $seconds, $microseconds));
}
/**
* Get the counter component
*/
public function getCounter(): int
{
return $this->counter;
}
/**
* Get the fingerprint component
*/
public function getFingerprint(): string
{
return $this->fingerprint;
}
/**
* Get the random component
*/
public function getRandom(): string
{
return $this->random;
}
/**
* Check equality with another Cuid
*/
public function equals(IdInterface $other): bool
{
if (! $other instanceof self) {
return false;
}
return $this->value === $other->value;
}
/**
* Compare with another Cuid for sorting
*/
public function compare(self $other): int
{
return strcmp($this->value, $other->value);
}
/**
* Check if this Cuid is older than another
*/
public function isOlderThan(self $other): bool
{
return $this->timestamp < $other->timestamp;
}
/**
* Check if this Cuid is newer than another
*/
public function isNewerThan(self $other): bool
{
return $this->timestamp > $other->timestamp;
}
/**
* Get age in milliseconds
*/
public function getAgeInMilliseconds(): int
{
return intval(microtime(true) * 1000) - $this->timestamp;
}
/**
* Get age in seconds
*/
public function getAgeInSeconds(): float
{
return $this->getAgeInMilliseconds() / 1000.0;
}
/**
* Check if Cuid is from the same process/machine (same fingerprint)
*/
public function isSameProcess(self $other): bool
{
return $this->fingerprint === $other->fingerprint;
}
/**
* Validate Base36 characters (lowercase only)
*/
private function isValidBase36(string $value): bool
{
$pattern = '/^[' . preg_quote(self::ALPHABET, '/') . ']+$/';
return preg_match($pattern, $value) === 1;
}
/**
* Parse components from the Cuid value
*/
private function parseComponents(): void
{
$offset = 1; // Skip the 'c' prefix
// Extract timestamp (8 chars)
$timestampBase36 = substr($this->value, $offset, self::TIMESTAMP_LENGTH);
$this->timestamp = self::fromBase36($timestampBase36);
$offset += self::TIMESTAMP_LENGTH;
// Extract counter (4 chars)
$counterBase36 = substr($this->value, $offset, self::COUNTER_LENGTH);
$this->counter = self::fromBase36($counterBase36);
$offset += self::COUNTER_LENGTH;
// Extract fingerprint (4 chars)
$this->fingerprint = substr($this->value, $offset, self::FINGERPRINT_LENGTH);
$offset += self::FINGERPRINT_LENGTH;
// Extract random part (8 chars)
$this->random = substr($this->value, $offset, self::RANDOM_LENGTH);
}
/**
* Convert integer to Base36 with padding
*/
private static function toBase36(int $value, int $length): string
{
$base36 = base_convert((string)$value, 10, 36);
return str_pad($base36, $length, '0', STR_PAD_LEFT);
}
/**
* Convert Base36 string to integer
*/
private static function fromBase36(string $base36): int
{
return intval(base_convert($base36, 36, 10));
}
}

View File

@@ -0,0 +1,279 @@
<?php
declare(strict_types=1);
namespace App\Framework\Id\Cuid;
use App\Framework\Id\Contracts\IdGeneratorInterface;
use App\Framework\Random\RandomGenerator;
use InvalidArgumentException;
/**
* Cuid Generator Service
*
* Generates Collision-resistant Unique Identifiers with machine fingerprinting.
*/
final class CuidGenerator implements IdGeneratorInterface
{
private int $counter = 0;
private string $fingerprint;
public function __construct(
private readonly RandomGenerator $randomGenerator,
?string $customFingerprint = null
) {
$this->fingerprint = $customFingerprint ?? $this->generateFingerprint();
$this->counter = $this->randomGenerator->int(0, 36 ** Cuid::COUNTER_LENGTH - 1);
}
/**
* Generate a new Cuid
*/
public function generate(): Cuid
{
$timestamp = $this->getCurrentTimestamp();
$counter = $this->getNextCounter();
$random = $this->generateRandomPart();
return Cuid::fromComponents($timestamp, $counter, $this->fingerprint, $random);
}
/**
* Generate a Cuid at a specific timestamp (milliseconds)
*/
public function generateAt(int $timestampMs): Cuid
{
$counter = $this->getNextCounter();
$random = $this->generateRandomPart();
return Cuid::fromComponents($timestampMs, $counter, $this->fingerprint, $random);
}
/**
* Generate a Cuid in the past (useful for testing)
*/
public function generateInPast(int $millisecondsAgo): Cuid
{
$timestamp = $this->getCurrentTimestamp() - $millisecondsAgo;
return $this->generateAt($timestamp);
}
/**
* Generate a batch of Cuids with incrementing counters
* @return array<int, Cuid>
*/
public function generateBatch(int $count): array
{
if ($count <= 0) {
throw new InvalidArgumentException('Count must be positive');
}
if ($count > 10000) {
throw new InvalidArgumentException('Batch size cannot exceed 10000');
}
$timestamp = $this->getCurrentTimestamp();
$cuids = [];
for ($i = 0; $i < $count; $i++) {
$counter = $this->getNextCounter();
$random = $this->generateRandomPart();
$cuids[] = Cuid::fromComponents($timestamp, $counter, $this->fingerprint, $random);
}
return $cuids;
}
/**
* Generate a sequence of Cuids with incrementing timestamps
* @return array<int, Cuid>
*/
public function generateSequence(int $count, int $intervalMs = 1): array
{
if ($count <= 0) {
throw new InvalidArgumentException('Count must be positive');
}
if ($count > 1000) {
throw new InvalidArgumentException('Sequence size cannot exceed 1000');
}
if ($intervalMs < 0) {
throw new InvalidArgumentException('Interval must be non-negative');
}
$cuids = [];
$timestamp = $this->getCurrentTimestamp();
for ($i = 0; $i < $count; $i++) {
$currentTimestamp = $timestamp + ($i * $intervalMs);
$cuids[] = $this->generateAt($currentTimestamp);
}
return $cuids;
}
/**
* Generate a Cuid with a specific counter value (for testing)
*/
public function generateWithCounter(int $counter): Cuid
{
if ($counter < 0 || $counter >= 36 ** Cuid::COUNTER_LENGTH) {
throw new InvalidArgumentException('Counter must be between 0 and ' . (36 ** Cuid::COUNTER_LENGTH - 1));
}
$timestamp = $this->getCurrentTimestamp();
$random = $this->generateRandomPart();
return Cuid::fromComponents($timestamp, $counter, $this->fingerprint, $random);
}
/**
* Parse a Cuid string and validate it
*/
public function parse(string $cuidString): Cuid
{
return Cuid::fromString($cuidString);
}
/**
* Validate if a string is a valid Cuid
*/
public function isValid(string $value): bool
{
if (strlen($value) !== Cuid::LENGTH) {
return false;
}
if (! str_starts_with($value, Cuid::PREFIX)) {
return false;
}
try {
Cuid::fromString($value);
return true;
} catch (InvalidArgumentException) {
return false;
}
}
/**
* Get the current fingerprint
*/
public function getFingerprint(): string
{
return $this->fingerprint;
}
/**
* Get the current counter value
*/
public function getCurrentCounter(): int
{
return $this->counter;
}
/**
* Reset the counter (useful for testing)
*/
public function resetCounter(): void
{
$this->counter = 0;
}
/**
* Check if two Cuids could have been generated by the same generator
*/
public function isSameGenerator(Cuid $cuid): bool
{
return $cuid->getFingerprint() === $this->fingerprint;
}
/**
* Get current timestamp in milliseconds
*/
private function getCurrentTimestamp(): int
{
return intval(microtime(true) * 1000);
}
/**
* Get next counter value with wraparound
*/
private function getNextCounter(): int
{
$current = $this->counter;
$this->counter = ($this->counter + 1) % (36 ** Cuid::COUNTER_LENGTH);
return $current;
}
/**
* Generate random part (8 chars Base36)
*/
private function generateRandomPart(): string
{
$random = '';
for ($i = 0; $i < Cuid::RANDOM_LENGTH; $i++) {
$randomIndex = $this->randomGenerator->int(0, 35);
$random .= Cuid::ALPHABET[$randomIndex];
}
return $random;
}
/**
* Generate machine fingerprint based on system characteristics
*/
private function generateFingerprint(): string
{
// Create a unique fingerprint based on system characteristics
$data = [
php_uname('n'), // hostname
php_uname('s'), // OS
php_uname('r'), // release
php_uname('m'), // machine type
getmypid(), // process ID
$_SERVER['SERVER_ADDR'] ?? 'localhost',
$_SERVER['SERVER_PORT'] ?? '80',
];
$hash = hash('sha256', implode('|', $data));
// Convert first 16 chars of hash to Base36
$fingerprint = '';
for ($i = 0; $i < Cuid::FINGERPRINT_LENGTH; $i++) {
$hexPair = substr($hash, $i * 2, 2);
$decimal = hexdec($hexPair);
$base36Char = base_convert((string)($decimal % 36), 10, 36);
$fingerprint .= $base36Char;
}
return $fingerprint;
}
/**
* Create factory method
*/
public static function create(RandomGenerator $randomGenerator, ?string $customFingerprint = null): self
{
return new self($randomGenerator, $customFingerprint);
}
/**
* Create with a deterministic fingerprint (for testing)
*/
public static function createWithFingerprint(RandomGenerator $randomGenerator, string $fingerprint): self
{
if (strlen($fingerprint) !== Cuid::FINGERPRINT_LENGTH) {
throw new InvalidArgumentException('Fingerprint must be exactly ' . Cuid::FINGERPRINT_LENGTH . ' characters');
}
return new self($randomGenerator, $fingerprint);
}
}

View File

@@ -0,0 +1,91 @@
<?php
declare(strict_types=1);
namespace App\Framework\Id;
use App\Framework\DateTime\Clock;
use App\Framework\DateTime\SystemClock;
use App\Framework\Id\Contracts\IdGeneratorInterface;
use App\Framework\Id\Cuid\CuidGenerator;
use App\Framework\Id\Ksuid\KsuidGenerator;
use App\Framework\Id\NanoId\NanoId;
use App\Framework\Id\NanoId\NanoIdGenerator;
use App\Framework\Id\Ulid\UlidGenerator;
use App\Framework\Random\RandomGenerator;
use App\Framework\Random\SecureRandomGenerator;
/**
* Factory for creating ID generators
*/
final readonly class IdGeneratorFactory
{
public function __construct(
private RandomGenerator $randomGenerator,
private ?Clock $clock = null
) {
}
/**
* Create a generator for the specified ID type
*
* @param IdType|string $type The ID type to create a generator for
* @return IdGeneratorInterface
*/
public function create(IdType|string $type): IdGeneratorInterface
{
if (is_string($type)) {
$type = IdType::fromString($type);
}
return match ($type) {
IdType::CUID => new CuidGenerator($this->randomGenerator),
IdType::KSUID => new KsuidGenerator($this->randomGenerator),
IdType::NANOID => new NanoIdGenerator($this->randomGenerator),
IdType::ULID => new UlidGenerator($this->clock ?? new SystemClock()),
};
}
/**
* Create a CUID generator
*/
public function createCuid(?string $customFingerprint = null): CuidGenerator
{
return new CuidGenerator($this->randomGenerator, $customFingerprint);
}
/**
* Create a KSUID generator
*/
public function createKsuid(): KsuidGenerator
{
return new KsuidGenerator($this->randomGenerator);
}
/**
* Create a NanoId generator
*/
public function createNanoId(int $defaultSize = NanoId::DEFAULT_SIZE, string $defaultAlphabet = NanoId::DEFAULT_ALPHABET): NanoIdGenerator
{
return new NanoIdGenerator($this->randomGenerator, $defaultSize, $defaultAlphabet);
}
/**
* Create a ULID generator
*/
public function createUlid(?Clock $clock = null): UlidGenerator
{
return new UlidGenerator($clock ?? $this->clock ?? new SystemClock());
}
/**
* Create a factory with default dependencies
*/
public static function createDefault(): self
{
return new self(
new SecureRandomGenerator(),
new SystemClock()
);
}
}

View File

@@ -0,0 +1,56 @@
<?php
declare(strict_types=1);
namespace App\Framework\Id;
/**
* Enum for ID types supported by the framework
*/
enum IdType: string
{
case CUID = 'cuid';
case KSUID = 'ksuid';
case NANOID = 'nanoid';
case ULID = 'ulid';
/**
* Get all available ID types
*
* @return array<string>
*/
public static function all(): array
{
return array_column(self::cases(), 'value');
}
/**
* Check if a string value is a valid ID type
*/
public static function isValid(string $value): bool
{
foreach (self::cases() as $case) {
if ($case->value === strtolower($value)) {
return true;
}
}
return false;
}
/**
* Create from string value
*/
public static function fromString(string $value): self
{
$value = strtolower($value);
foreach (self::cases() as $case) {
if ($case->value === $value) {
return $case;
}
}
throw new \InvalidArgumentException("Invalid ID type: {$value}. Valid types are: " . implode(', ', self::all()));
}
}

View File

@@ -0,0 +1,305 @@
<?php
declare(strict_types=1);
namespace App\Framework\Id\Ksuid;
use App\Framework\Id\Contracts\IdInterface;
use BcMath\Number;
use DateTimeImmutable;
use InvalidArgumentException;
use Stringable;
/**
* KSUID Value Object
*
* K-Sortable Unique Identifier
* - 160-bit (27 characters Base62)
* - 32-bit timestamp + 128-bit random payload
* - Lexicographically sortable by creation time
* - URL-safe, case-sensitive
*/
final readonly class Ksuid implements IdInterface, Stringable
{
public const int ENCODED_LENGTH = 27;
public const int TIMESTAMP_BYTES = 4;
public const int PAYLOAD_BYTES = 16;
public const int TOTAL_BYTES = self::TIMESTAMP_BYTES + self::PAYLOAD_BYTES; // 20
// KSUID epoch: 2014-05-13T16:53:20Z (Unix timestamp 1400000000)
public const int EPOCH = 1400000000;
// Base62 alphabet: 0-9, A-Z, a-z
public const string ALPHABET = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz';
private string $value;
private int $timestamp;
private string $payload;
public function __construct(string $value)
{
if (empty($value)) {
throw new InvalidArgumentException('KSUID cannot be empty');
}
if (strlen($value) !== self::ENCODED_LENGTH) {
throw new InvalidArgumentException('KSUID must be exactly ' . self::ENCODED_LENGTH . ' characters long');
}
if (! $this->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(IdInterface $other): bool
{
if (! $other instanceof self) {
return false;
}
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);
}
}

View File

@@ -0,0 +1,209 @@
<?php
declare(strict_types=1);
namespace App\Framework\Id\Ksuid;
use App\Framework\Id\Contracts\IdGeneratorInterface;
use App\Framework\Random\RandomGenerator;
use DateTimeImmutable;
use InvalidArgumentException;
/**
* KSUID Generator Service
*
* Generates K-Sortable Unique Identifiers with timestamp ordering.
*/
final readonly class KsuidGenerator implements IdGeneratorInterface
{
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\Id\Ksuid\Ksuid, max: \App\Framework\Id\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);
}
}

View File

@@ -0,0 +1,174 @@
<?php
declare(strict_types=1);
namespace App\Framework\Id\NanoId;
use App\Framework\Id\Contracts\IdInterface;
use InvalidArgumentException;
/**
* NanoId Value Object
*
* A URL-safe, unique string ID generator.
* Default alphabet: A-Za-z0-9_-
* Default size: 21 characters
*/
final readonly class NanoId implements IdInterface
{
public const int DEFAULT_SIZE = 21;
public const string DEFAULT_ALPHABET = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz_-';
// URL-safe alphabet without lookalikes
public const string SAFE_ALPHABET = '23456789ABCDEFGHJKLMNPQRSTWXYZabcdefghijkmnpqrstwxyz';
// Numbers only
public const string NUMBERS = '0123456789';
// Lowercase alphanumeric
public const string LOWERCASE_ALPHANUMERIC = '0123456789abcdefghijklmnopqrstuvwxyz';
// Uppercase alphanumeric
public const string UPPERCASE_ALPHANUMERIC = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ';
private string $value;
public function __construct(string $value)
{
if (empty($value)) {
throw new InvalidArgumentException('NanoId cannot be empty');
}
if (strlen($value) > 255) {
throw new InvalidArgumentException('NanoId cannot exceed 255 characters');
}
$this->value = $value;
}
/**
* Create from an existing string
*/
public static function fromString(string $value): self
{
return new self($value);
}
/**
* Validate if a string matches a specific alphabet
*/
public function matchesAlphabet(string $alphabet): bool
{
$pattern = '/^[' . preg_quote($alphabet, '/') . ']+$/';
return preg_match($pattern, $this->value) === 1;
}
/**
* Check if this is a valid default NanoId
*/
public function isDefault(): bool
{
return $this->matchesAlphabet(self::DEFAULT_ALPHABET);
}
/**
* Check if this is a safe NanoId (no lookalikes)
*/
public function isSafe(): bool
{
return $this->matchesAlphabet(self::SAFE_ALPHABET);
}
/**
* Check if this is a numeric NanoId
*/
public function isNumeric(): bool
{
return ctype_digit($this->value);
}
/**
* Get the length of the NanoId
*/
public function getLength(): int
{
return strlen($this->value);
}
/**
* Get the string value
*/
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;
}
/**
* Check equality with another NanoId
*/
public function equals(IdInterface $other): bool
{
if (! $other instanceof self) {
return false;
}
return $this->value === $other->value;
}
/**
* Create a prefixed NanoId
*/
public function withPrefix(string $prefix): self
{
if (empty($prefix)) {
throw new InvalidArgumentException('Prefix cannot be empty');
}
return new self($prefix . $this->value);
}
/**
* Create a suffixed NanoId
*/
public function withSuffix(string $suffix): self
{
if (empty($suffix)) {
throw new InvalidArgumentException('Suffix cannot be empty');
}
return new self($this->value . $suffix);
}
/**
* Get a truncated version of the NanoId
*/
public function truncate(int $length): self
{
if ($length <= 0) {
throw new InvalidArgumentException('Length must be positive');
}
if ($length >= strlen($this->value)) {
return $this;
}
return new self(substr($this->value, 0, $length));
}
}

View File

@@ -0,0 +1,247 @@
<?php
declare(strict_types=1);
namespace App\Framework\Id\NanoId;
use App\Framework\Id\Contracts\IdGeneratorInterface;
use App\Framework\Random\RandomGenerator;
use InvalidArgumentException;
/**
* NanoId Generator Service
*
* Provides flexible NanoId generation with various presets and configurations.
*/
final readonly class NanoIdGenerator implements IdGeneratorInterface
{
private int $defaultSize;
private string $defaultAlphabet;
public function __construct(
private RandomGenerator $randomGenerator,
int $defaultSize = NanoId::DEFAULT_SIZE,
string $defaultAlphabet = NanoId::DEFAULT_ALPHABET
) {
if ($defaultSize <= 0 || $defaultSize > 255) {
throw new InvalidArgumentException('Default size must be between 1 and 255');
}
if (empty($defaultAlphabet)) {
throw new InvalidArgumentException('Default alphabet cannot be empty');
}
$this->defaultSize = $defaultSize;
$this->defaultAlphabet = $defaultAlphabet;
}
/**
* Generate a NanoId with default settings
*/
public function generate(): NanoId
{
return $this->generateWithCustom($this->defaultSize, $this->defaultAlphabet);
}
/**
* Generate a NanoId with custom size
*/
public function generateWithSize(int $size): NanoId
{
return $this->generateWithCustom($size, $this->defaultAlphabet);
}
/**
* Generate a NanoId with custom alphabet
*/
public function generateWithAlphabet(string $alphabet): NanoId
{
return $this->generateWithCustom($this->defaultSize, $alphabet);
}
/**
* Generate a NanoId with custom size and alphabet
*/
public function generateCustom(int $size, string $alphabet): NanoId
{
return $this->generateWithCustom($size, $alphabet);
}
/**
* Core NanoId generation logic
*/
private function generateWithCustom(int $size, string $alphabet): NanoId
{
if ($size <= 0 || $size > 255) {
throw new InvalidArgumentException('Size must be between 1 and 255');
}
if (empty($alphabet)) {
throw new InvalidArgumentException('Alphabet cannot be empty');
}
$alphabetLength = strlen($alphabet);
if ($alphabetLength > 255) {
throw new InvalidArgumentException('Alphabet cannot exceed 255 characters');
}
$id = '';
$mask = (2 << (int)(log($alphabetLength - 1) / M_LN2)) - 1;
$step = (int)ceil(1.6 * $mask * $size / $alphabetLength);
while (strlen($id) < $size) {
$bytes = $this->randomGenerator->bytes($step);
for ($i = 0; $i < $step && strlen($id) < $size; $i++) {
$byte = ord($bytes[$i]) & $mask;
if ($byte < $alphabetLength) {
$id .= $alphabet[$byte];
}
}
}
return NanoId::fromString($id);
}
/**
* Generate a safe NanoId (no lookalikes)
*/
public function generateSafe(int $size = NanoId::DEFAULT_SIZE): NanoId
{
return $this->generateWithCustom($size, NanoId::SAFE_ALPHABET);
}
/**
* Generate a numeric NanoId
*/
public function generateNumeric(int $size = 12): NanoId
{
return $this->generateWithCustom($size, NanoId::NUMBERS);
}
/**
* Generate a lowercase alphanumeric NanoId
*/
public function generateLowercase(int $size = NanoId::DEFAULT_SIZE): NanoId
{
return $this->generateWithCustom($size, NanoId::LOWERCASE_ALPHANUMERIC);
}
/**
* Generate an uppercase alphanumeric NanoId
*/
public function generateUppercase(int $size = NanoId::DEFAULT_SIZE): NanoId
{
return $this->generateWithCustom($size, NanoId::UPPERCASE_ALPHANUMERIC);
}
/**
* Generate a NanoId for a specific entity type with prefix
*/
public function generateForEntity(string $entityType, int $size = 16): NanoId
{
$prefix = match($entityType) {
'user' => 'usr_',
'order' => 'ord_',
'product' => 'prd_',
'session' => 'ses_',
'token' => 'tok_',
'transaction' => 'txn_',
'invoice' => 'inv_',
'customer' => 'cus_',
'payment' => 'pay_',
'subscription' => 'sub_',
default => strtolower(substr($entityType, 0, 3)) . '_'
};
$id = $this->generateWithCustom($size, NanoId::DEFAULT_ALPHABET);
return $id->withPrefix($prefix);
}
/**
* Generate a time-prefixed NanoId (sortable by creation time)
*/
public function generateTimePrefixed(int $idSize = 12): NanoId
{
// Use base36 timestamp for compactness
$timestamp = base_convert((string)time(), 10, 36);
$id = $this->generateWithCustom($idSize, NanoId::LOWERCASE_ALPHANUMERIC);
return $id->withPrefix($timestamp . '_');
}
/**
* Generate a batch of unique NanoIds
*/
public function generateBatch(int $count, int $size = NanoId::DEFAULT_SIZE): array
{
if ($count <= 0) {
throw new InvalidArgumentException('Count must be positive');
}
if ($count > 10000) {
throw new InvalidArgumentException('Batch size cannot exceed 10000');
}
$ids = [];
$generated = [];
while (count($ids) < $count) {
$id = $this->generateWithCustom($size, $this->defaultAlphabet);
$value = $id->toString();
// Ensure uniqueness within batch
if (! isset($generated[$value])) {
$ids[] = $id;
$generated[$value] = true;
}
}
return $ids;
}
/**
* Validate if a string is a valid NanoId for the current configuration
*/
public function isValid(string $value): bool
{
if (empty($value) || strlen($value) > 255) {
return false;
}
try {
$nanoId = NanoId::fromString($value);
return $nanoId->matchesAlphabet($this->defaultAlphabet);
} catch (InvalidArgumentException) {
return false;
}
}
/**
* Create a custom generator with specific settings
*/
public static function create(RandomGenerator $randomGenerator, int $size = NanoId::DEFAULT_SIZE, string $alphabet = NanoId::DEFAULT_ALPHABET): self
{
return new self($randomGenerator, $size, $alphabet);
}
/**
* Create a generator for safe IDs (no lookalikes)
*/
public static function createSafe(RandomGenerator $randomGenerator, int $size = NanoId::DEFAULT_SIZE): self
{
return new self($randomGenerator, $size, NanoId::SAFE_ALPHABET);
}
/**
* Create a generator for numeric IDs
*/
public static function createNumeric(RandomGenerator $randomGenerator, int $size = 12): self
{
return new self($randomGenerator, $size, NanoId::NUMBERS);
}
}

View File

@@ -0,0 +1,56 @@
<?php
declare(strict_types=1);
namespace App\Framework\Id\Ulid;
use App\Framework\Core\Encoding\Base32Alphabet;
use App\Framework\Core\Encoding\Base32Encoder;
/**
* ULID String Converter using Crockford's Base32 alphabet
*
* Migrated to use the framework's Base32Encoder with Crockford alphabet
* for consistency and better maintainability.
*/
final readonly class StringConverter
{
/**
* Encodes a binary string into a Base32 encoded string using Crockford's alphabet
*
* @param string $binary The binary string to be encoded.
* @return string The Base32 encoded string.
*/
public function encodeBase32(string $binary): string
{
return Base32Encoder::encodeCrockford($binary);
}
/**
* Decodes a Base32-encoded string into its binary representation
*
* @param string $base32 The Base32-encoded string to decode.
* @return string The binary representation of the decoded string.
* @throws \InvalidArgumentException If the provided string contains invalid Base32 characters.
*/
public function decodeBase32(string $base32): string
{
return Base32Encoder::decodeCrockford($base32);
}
/**
* Validate if string is valid Crockford Base32
*/
public function isValidBase32(string $base32): bool
{
return Base32Alphabet::CROCKFORD->isValidEncoded($base32);
}
/**
* Get the Crockford Base32 alphabet used by ULID
*/
public function getAlphabet(): string
{
return Base32Alphabet::CROCKFORD->getAlphabet();
}
}

View File

@@ -0,0 +1,132 @@
<?php
declare(strict_types=1);
namespace App\Framework\Id\Ulid;
use App\Framework\DateTime\Clock;
use App\Framework\Id\Contracts\IdInterface;
use DateTimeImmutable;
use InvalidArgumentException;
use JsonSerializable;
/**
* Objekt-Wrapper für ULIDs mit String-/JSON-API.
*/
final readonly class Ulid implements IdInterface, JsonSerializable
{
private string $ulid;
private StringConverter $converter;
public function __construct(
private Clock $clock,
?string $ulid = null,
bool $isBase32 = false,
) {
$gen = new UlidGenerator();
$validator = new UlidValidator();
$this->converter = new StringConverter();
if ($ulid === null) {
$this->ulid = $gen->generate($this->clock);
} else {
if ($isBase32) {
$this->ulid = $this->converter->encodeBase32($ulid);
return;
}
if (! $validator->isValid($ulid)) {
throw new InvalidArgumentException("Ungültige ULID: $ulid");
}
$this->ulid = $ulid;
}
}
public static function fromBase32(Clock $clock, string $ulid): self
{
return new self($clock, $ulid, true);
}
public function toBinary(): string
{
return $this->converter->decodeBase32($this->ulid);
}
public function __toString(): string
{
return $this->ulid;
}
/**
* @inheritDoc
*/
public function toString(): string
{
return $this->ulid;
}
/**
* @inheritDoc
*/
public function getValue(): string
{
return $this->ulid;
}
/**
* @inheritDoc
*/
public function equals(IdInterface $other): bool
{
if (! $other instanceof self) {
return false;
}
return $this->ulid === $other->ulid;
}
public function jsonSerialize(): string
{
return $this->ulid;
}
public function getTimestampMs(): int
{
$parser = new ULIDParser(new ULIDValidator());
return $parser->getTimestampMs($this->ulid);
}
public function getDateTime(): DateTimeImmutable
{
$parser = new ULIDParser(new ULIDValidator());
return $parser->getDateTime($this->ulid, $this->clock);
}
public static function isValid(string $ulid): bool
{
return new ULIDValidator()->isValid($ulid);
}
public static function fromString(Clock $clock, string $ulid): self
{
return new self($clock, $ulid);
}
public function __debugInfo(): ?array
{
return [
'ulid' => $this->ulid,
'timestamp' => $this->getTimestampMs(),
'clock' => $this->clock,
'datetime' => $this->getDateTime(),
];
}
}

View File

@@ -0,0 +1,95 @@
<?php
declare(strict_types=1);
namespace App\Framework\Id\Ulid;
use App\Framework\DateTime\Clock;
use App\Framework\DateTime\SystemClock;
use App\Framework\Id\Contracts\IdGeneratorInterface;
/**
* ULID Generator - Universally Unique Lexicographically Sortable Identifier
*
* Generates 26-character, timestamp-based, sortable unique identifiers.
* Drop-in replacement for deprecated uniqid() function.
*
* Usage:
* - Production: new UlidGenerator() - uses SystemClock automatically
* - Testing: new UlidGenerator($mockClock) - inject mock for deterministic tests
*/
final readonly class UlidGenerator implements IdGeneratorInterface
{
public function __construct(
private ?Clock $clock = null
) {}
/**
* Generate a new ULID
*
* @return string 26-character ULID (Base32 encoded)
*/
public function generate(): string
{
$clock = $this->clock ?? new SystemClock();
$stringConverter = new StringConverter();
// Get timestamp in milliseconds (ULID uses millisecond precision)
$timestamp = $clock->now()->getTimestamp() . $clock->now()->getMicrosecond();
$time = (int)$timestamp / 1000;
// Pack timestamp as 48-bit big-endian integer
$timeBin = substr(pack('J', $time), 2, 6);
// 80 bits of cryptographically secure randomness
$random = random_bytes(10);
// Combine timestamp and random bytes
$bin = $timeBin . $random;
return $stringConverter->encodeBase32($bin);
}
/**
* Generate a ULID with a prefix
*
* Useful for namespacing/categorizing IDs (e.g., "user_01ARZ3NDEKTSV4...")
*
* @param string $prefix Prefix to prepend (without separator)
* @return string Prefixed ULID
*/
public function generateWithPrefix(string $prefix): string
{
return $prefix . '_' . $this->generate();
}
/**
* @inheritDoc
*/
public function generateBatch(int $count): array
{
if ($count <= 0) {
throw new \InvalidArgumentException('Count must be positive');
}
if ($count > 10000) {
throw new \InvalidArgumentException('Batch size cannot exceed 10000');
}
$ids = [];
for ($i = 0; $i < $count; $i++) {
$ids[] = $this->generate();
}
return $ids;
}
/**
* @inheritDoc
*/
public function isValid(string $value): bool
{
return \App\Framework\Id\Ulid\Ulid::isValid($value);
}
}

View File

@@ -0,0 +1,77 @@
<?php
declare(strict_types=1);
namespace App\Framework\Id\Ulid;
use App\Framework\DateTime\Clock;
use DateTimeImmutable;
use InvalidArgumentException;
/**
* Extrahiert Zeitstempel aus einer validen ULID.
*/
final readonly class UlidParser
{
public function __construct(
private UlidValidator $validator
) {
}
/**
* @param string $ulid
* @return int Millisekunden seit Unix-Epoch
* @throws InvalidArgumentException
*/
public function getTimestampMs(string $ulid): int
{
if (! $this->validator->isValid($ulid)) {
throw new InvalidArgumentException("Ungültige ULID: $ulid");
}
// erste 10 Zeichen = 10×5=50 Bit, wir brauchen die ersten 48 Bit
$bits = '';
for ($i = 0; $i < 10; $i++) {
$bits .= str_pad(decbin($this->charValue($ulid[$i])), 5, '0', STR_PAD_LEFT);
}
$timeBits = substr($bits, 0, 48);
return bindec($timeBits);
}
/**
* @param string $ulid
* @param Clock $clock
* @return DateTimeImmutable
*/
public function getDateTime(string $ulid, Clock $clock): DateTimeImmutable
{
$ms = $this->getTimestampMs($ulid);
$sec = floor($ms / 1000);
$usec = ($ms % 1000) * 1000;
return $clock->fromString(sprintf("%d %06d", $sec, $usec), 'U u');
}
/**
* Converts a Base32 character into its corresponding integer value.
*
* @param string $char The Base32 character to convert.
* @return int The integer value of the specified Base32 character.
* @throws InvalidArgumentException If the provided character is not a valid Base32 character.
*/
private function charValue(string $char): int
{
static $map = null;
if ($map === null) {
$alphabet = '0123456789ABCDEFGHJKMNPQRSTVWXYZ';
$map = array_flip(str_split($alphabet));
}
$upper = strtoupper($char);
if (! isset($map[$upper])) {
throw new InvalidArgumentException("Ungültiges Base32-Zeichen: $char");
}
return $map[$upper];
}
}

View File

@@ -0,0 +1,20 @@
<?php
declare(strict_types=1);
namespace App\Framework\Id\Ulid;
/**
* Validiert ULID-Strings.
*/
final readonly class UlidValidator
{
/**
* @param string $ulid
* @return bool true, wenn $ulid exakt 26 Zeichen Crockford-Base32 sind
*/
public function isValid(string $ulid): bool
{
return (bool) preg_match('/^[0-9A-HJKMNP-TV-Z]{26}$/', $ulid);
}
}

View File

@@ -0,0 +1,121 @@
<?php
declare(strict_types=1);
namespace App\Framework\Id;
use App\Framework\Id\Contracts\IdGeneratorInterface;
use App\Framework\Id\Contracts\IdInterface;
use InvalidArgumentException;
/**
* Unified ID Generator
*
* A wrapper that can generate any type of ID based on configuration.
* Provides a single interface for generating different ID formats.
*/
final readonly class UnifiedIdGenerator implements IdGeneratorInterface
{
public function __construct(
private IdGeneratorFactory $factory,
private IdType $defaultType = IdType::ULID
) {
}
/**
* Generate a new ID using the default type
*/
public function generate(): IdInterface|string
{
return $this->generateWithType($this->defaultType);
}
/**
* Generate a new ID of the specified type
*
* @param IdType|string $type The ID type to generate
* @return IdInterface|string
*/
public function generateWithType(IdType|string $type): IdInterface|string
{
$generator = $this->factory->create($type);
return $generator->generate();
}
/**
* Generate a batch of IDs using the default type
*
* @param int $count Number of IDs to generate
* @return array<int, IdInterface|string>
*/
public function generateBatch(int $count): array
{
return $this->generateBatchWithType($count, $this->defaultType);
}
/**
* Generate a batch of IDs of the specified type
*
* @param int $count Number of IDs to generate
* @param IdType|string $type The ID type to generate
* @return array<int, IdInterface|string>
*/
public function generateBatchWithType(int $count, IdType|string $type): array
{
$generator = $this->factory->create($type);
return $generator->generateBatch($count);
}
/**
* Validate if a string is a valid ID for the default type
*/
public function isValid(string $value): bool
{
return $this->isValidForType($value, $this->defaultType);
}
/**
* Validate if a string is a valid ID for the specified type
*
* @param string $value The ID string to validate
* @param IdType|string $type The ID type to validate against
*/
public function isValidForType(string $value, IdType|string $type): bool
{
$generator = $this->factory->create($type);
return $generator->isValid($value);
}
/**
* Get the default ID type
*/
public function getDefaultType(): IdType
{
return $this->defaultType;
}
/**
* Create a unified generator with default settings
*/
public static function createDefault(?IdType $defaultType = null): self
{
return new self(
IdGeneratorFactory::createDefault(),
$defaultType ?? IdType::ULID
);
}
/**
* Create a unified generator with a custom factory
*/
public static function create(IdGeneratorFactory $factory, ?IdType $defaultType = null): self
{
return new self(
$factory,
$defaultType ?? IdType::ULID
);
}
}