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:
169
src/Framework/NanoId/NanoId.php
Normal file
169
src/Framework/NanoId/NanoId.php
Normal file
@@ -0,0 +1,169 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\NanoId;
|
||||
|
||||
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
|
||||
{
|
||||
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(self $other): bool
|
||||
{
|
||||
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));
|
||||
}
|
||||
}
|
||||
246
src/Framework/NanoId/NanoIdGenerator.php
Normal file
246
src/Framework/NanoId/NanoIdGenerator.php
Normal file
@@ -0,0 +1,246 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\NanoId;
|
||||
|
||||
use App\Framework\Random\RandomGenerator;
|
||||
use InvalidArgumentException;
|
||||
|
||||
/**
|
||||
* NanoId Generator Service
|
||||
*
|
||||
* Provides flexible NanoId generation with various presets and configurations.
|
||||
*/
|
||||
final readonly class NanoIdGenerator
|
||||
{
|
||||
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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user