Files
michaelschiemer/src/Framework/NanoId/NanoIdGenerator.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

247 lines
6.8 KiB
PHP

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