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:
2025-08-11 20:13:26 +02:00
parent 59fd3dd3b1
commit 55a330b223
3683 changed files with 2956207 additions and 16948 deletions

275
src/Framework/Cuid/Cuid.php Normal file
View 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));
}
}

View 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);
}
}