171 lines
3.7 KiB
PHP
171 lines
3.7 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Framework\NanoId;
|
|
|
|
use InvalidArgumentException;
|
|
use Stringable;
|
|
|
|
/**
|
|
* 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 Stringable
|
|
{
|
|
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));
|
|
}
|
|
}
|