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
This commit is contained in:
275
src/Framework/Cuid/Cuid.php
Normal file
275
src/Framework/Cuid/Cuid.php
Normal file
@@ -0,0 +1,275 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\Cuid;
|
||||
|
||||
use DateTimeImmutable;
|
||||
use InvalidArgumentException;
|
||||
|
||||
/**
|
||||
* 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
|
||||
{
|
||||
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(self $other): bool
|
||||
{
|
||||
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));
|
||||
}
|
||||
}
|
||||
276
src/Framework/Cuid/CuidGenerator.php
Normal file
276
src/Framework/Cuid/CuidGenerator.php
Normal file
@@ -0,0 +1,276 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\Cuid;
|
||||
|
||||
use App\Framework\Random\RandomGenerator;
|
||||
use InvalidArgumentException;
|
||||
|
||||
/**
|
||||
* Cuid Generator Service
|
||||
*
|
||||
* Generates Collision-resistant Unique Identifiers with machine fingerprinting.
|
||||
*/
|
||||
final class CuidGenerator
|
||||
{
|
||||
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
|
||||
*/
|
||||
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
|
||||
*/
|
||||
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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user