Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 1 addition & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,7 @@ All-in-one unique ID toolkit for PHP.

- UUID (`v1`, `v3`, `v4`, `v5`, `v6`, `v7`, `v8`)
- ULID (monotonic and random modes)
- Snowflake, Sonyflake, TBSL
- Randflake (encrypted 64-bit IDs with lease-bound node windows)
- Snowflake, Sonyflake, Randflake, TBSL
- NanoID, CUID2, KSUID, XID
- Opaque and deterministic IDs
- Value objects and comparator utilities
Expand Down
2 changes: 1 addition & 1 deletion composer.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "infocyph/uid",
"description": "UUID (RFC 9562), ULID, Snowflake ID, Sonyflake ID, and TBSL generator for PHP.",
"description": "UUID (RFC 9562), ULID, Snowflake, Sonyflake, Randflake, and TBSL generator for PHP.",
"license": "MIT",
"type": "library",
"keywords": [
Expand Down
9 changes: 7 additions & 2 deletions docs/randflake.rst
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,19 @@ Class: ``Infocyph\\UID\\Randflake``
Overview
--------

Randflake is a lease-bound 64-bit ID family with encrypted payload fields.
Randflake is a lease-bound 64-bit ID family whose payload fields are obscured by
a reversible keyed permutation.

Layout before encryption:
Layout before permutation:

- 30 bits timestamp (seconds from epoch offset ``1730000000``)
- 17 bits node ID
- 17 bits sequence

The permutation is not authenticated encryption: it does not prove integrity,
authorize access, or replace a standard encryption protocol. Treat Randflake
values as identifiers and enforce authorization independently.

Generation
----------

Expand Down
11 changes: 10 additions & 1 deletion docs/random-ids.rst
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,9 @@ Classes:
$isCuid2 = CUID2::isValid($cuid2);
$cuid2Info = CUID2::parse($cuid2);

CUID2 lengths are ``2..32`` and canonical values always begin with a lowercase
letter.

Validation/parsing outputs:

- ``NanoID::parse()``: ``['isValid', 'length', 'alphabet']``
Expand Down Expand Up @@ -82,6 +85,9 @@ Class: ``Infocyph\\UID\\OpaqueId``
- ``OpaqueId::random($length)`` for short public IDs
- ``OpaqueId::fromInt($value, $salt)`` and ``OpaqueId::toInt($token, $salt)`` for reversible obfuscation

The reversible integer form is obfuscation only. It is not encryption,
authentication, or authorization; always enforce access control independently.

.. code-block:: php

<?php
Expand All @@ -97,7 +103,10 @@ Deterministic IDs
Class: ``Infocyph\\UID\\DeterministicId``

``DeterministicId::fromPayload($payload, $length = 24, $namespace = 'default')``
creates deterministic Base62 output from payload + namespace.
creates deterministic Base62 output from payload + namespace. Lengths are
``1..43``; shorter outputs have a correspondingly higher collision probability.
The namespace delimiter ``|`` is reserved so namespace/payload pairs remain
unambiguous.

.. code-block:: php

Expand Down
9 changes: 7 additions & 2 deletions docs/sequence-providers.rst
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ Each algorithm class using ``GetSequence`` provides these static methods:
- ``resetSequenceProvider()``
- ``useFilesystemSequenceProvider(?string $baseDirectory = null, int $waitTime = 1000, int $maxAttempts = 1000)``
- ``useInMemorySequenceProvider()``
- ``useSimpleCacheSequenceProvider(CacheInterface $cache, string $prefix = 'uid:seq:', int $waitTime = 1000, int $maxAttempts = 1000)``
- ``useSimpleCacheSequenceProvider(CacheInterface $cache, string $prefix = 'uid.seq.', int $waitTime = 1000, int $maxAttempts = 1000)``
- ``useSequenceCallback(callable $callback)``

Defaults are tuned for better contention tolerance in parallel workloads.
Expand Down Expand Up @@ -51,9 +51,14 @@ Example: PSR-16 Simple Cache
use Psr\SimpleCache\CacheInterface;

/** @var CacheInterface $cache */
$provider = new PsrSimpleCacheSequenceProvider($cache, 'uid:seq:');
$provider = new PsrSimpleCacheSequenceProvider($cache, 'uid.seq.');
Snowflake::setSequenceProvider($provider);

The default cache prefix uses only the portable PSR-16 key character set.
The built-in fallback lock is local to one host. For a cache shared by multiple
hosts, provide a synchronizer backed by a distributed lock or use a custom
provider with an atomic increment contract.

Example: External Synchronizer
------------------------------

Expand Down
1 change: 1 addition & 0 deletions docs/ulid.rst
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ ULID
Class: ``Infocyph\\UID\\ULID``

ULID uses Crockford Base32 and produces 26-character sortable identifiers.
Timestamps must fit the protocol's unsigned 48-bit millisecond field.

Generation Modes
----------------
Expand Down
4 changes: 4 additions & 0 deletions docs/uuid.rst
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,10 @@ NIL and MAX
Validation and Parsing
----------------------

Validation accepts the RFC 9562 NIL and MAX values and UUID versions ``1..8``
using the RFC variant. UUIDv7 rejects timestamps outside its unsigned 48-bit
millisecond field instead of truncating them.

.. code-block:: php

<?php
Expand Down
80 changes: 34 additions & 46 deletions src/CUID2.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,37 +4,45 @@

namespace Infocyph\UID;

use DateTimeImmutable;
use Exception;
use Infocyph\UID\Contracts\IdAlgorithmInterface;
use Infocyph\UID\Support\BaseEncoder;
use InvalidArgumentException;

final class CUID2 implements IdAlgorithmInterface
{
private static string $alphabet = '0123456789abcdefghijklmnopqrstuvwxyz';
private const INITIAL_COUNTER_MAX = 476_782_367;

private const LETTERS = 'abcdefghijklmnopqrstuvwxyz';

private static int $counter;

private static ?string $fingerprint = null;

/**
* Generates a CUID2 string with a specified maximum length.
*
* @throws InvalidArgumentException|Exception
*/
public static function generate(int $length = 24): string
{
($length < 4 || $length > 32) && throw new InvalidArgumentException(
'length must be between 4 and 32',
($length < 2 || $length > 32) && throw new InvalidArgumentException(
'length must be between 2 and 32',
);

self::$counter ??= random_int(0, PHP_INT_MAX);
$hash = hash_init('sha3-512');
hash_update($hash, (new DateTimeImmutable('now'))->format('Uv'));
hash_update($hash, (string) self::$counter++);
hash_update($hash, bin2hex(random_bytes($length)));
hash_update($hash, self::fingerprint());
$encoded = self::hexToBase36(hash_final($hash));

return substr(str_pad($encoded, $length, '0'), 0, $length);
self::$counter ??= random_int(0, self::INITIAL_COUNTER_MAX);
$time = (string) (int) floor(microtime(true) * 1000);
$counter = (string) self::$counter++;
$salt = random_bytes($length);
$fingerprint = self::fingerprint();
$hashInput = pack('N', strlen($time)) . $time
. pack('N', strlen($counter)) . $counter
. pack('N', strlen($salt)) . $salt
. pack('N', strlen($fingerprint)) . $fingerprint;
$encoded = BaseEncoder::encodeBytes(hash('sha3-512', $hashInput, true), 36);
$firstLetter = self::LETTERS[random_int(0, 25)];

return $firstLetter . substr($encoded, 1, $length - 1);
}

/**
Expand All @@ -46,7 +54,7 @@ public static function isValid(string $id, ?int $length = null): bool
return false;
}

return preg_match('/^[0-9a-z]{4,32}$/', $id) === 1;
return preg_match('/^[a-z][0-9a-z]{1,31}$/D', $id) === 1;
}

/**
Expand All @@ -69,40 +77,20 @@ public static function parse(string $id, ?int $length = null): array
*/
private static function fingerprint(): string
{
$hash = hash_init('sha3-512');
hash_update($hash, gethostname() ?: substr(str_shuffle('abcdefghjkmnpqrstvwxyz0123456789'), 0, 32));
hash_update($hash, (string) random_int(1, 32768));
hash_update($hash, bin2hex(random_bytes(32)));

return hash_final($hash);
}

/**
* Converts a base16 string to base36 using BCMath for large integer safety.
*/
private static function hexToBase36(string $hex): string
{
$hex = strtolower(ltrim($hex, '0'));
if ($hex === '') {
return '0';
}

$decimal = '0';
$hexChars = '0123456789abcdef';
foreach (str_split($hex) as $char) {
($value = strpos($hexChars, $char)) !== false || throw new InvalidArgumentException(
'Invalid hexadecimal string provided',
);
$decimal = bcadd(bcmul($decimal, '16'), (string) $value);
if (self::$fingerprint !== null) {
return self::$fingerprint;
}

$encoded = '';
while (bccomp($decimal, '0') === 1) {
$remainder = (int) bcmod($decimal, '36');
$encoded = self::$alphabet[$remainder] . $encoded;
$decimal = bcdiv($decimal, '36', 0);
}
$host = gethostname();
$source = ($host === false ? '' : $host)
. "\0"
. getmypid()
. random_bytes(32);

return $encoded;
return self::$fingerprint = substr(
BaseEncoder::encodeBytes(hash('sha3-512', $source, true), 36),
0,
32,
);
}
}
5 changes: 4 additions & 1 deletion src/Configuration/ResolvesCustomEpoch.php
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,10 @@ private static function resolveEpochValue(DateTimeInterface|int|string|null $cus
}

$epoch = strtotime($customEpoch);
if ($epoch === false) {
throw new \InvalidArgumentException('Custom epoch must be a valid date string');
}

return $epoch === false ? null : $epoch * 1000;
return $epoch * 1000;
}
}
26 changes: 26 additions & 0 deletions src/Configuration/ResolvesMachineId.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<?php

declare(strict_types=1);

namespace Infocyph\UID\Configuration;

use Closure;

trait ResolvesMachineId
{
private readonly ?Closure $machineIdResolver;

public function resolveMachineId(): int
{
if ($this->machineIdResolver === null) {
return $this->machineId;
}

$machineId = ($this->machineIdResolver)();
if (!is_int($machineId)) {
throw new \UnexpectedValueException('Machine ID resolver must return an integer');
}

return $machineId;
}
}
15 changes: 11 additions & 4 deletions src/Configuration/SnowflakeConfig.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
private ?Closure $nodeResolver;

/**
* @param callable():array{0:int,1:int}|null $nodeResolver
* @param callable():mixed|null $nodeResolver
* @param DateTimeInterface|int|string|null $customEpoch Epoch in ms (int), parseable date string, or DateTime.
*/
public function __construct(
Expand All @@ -33,17 +33,24 @@ public function __construct(
}

/**
* @return array{0:int,1:int}
* @return array{0:int, 1:int}
*/
public function resolveNode(): array
{
if ($this->nodeResolver === null) {
return [$this->datacenterId, $this->workerId];
}

/** @var array{0:int,1:int} $resolved */
$resolved = ($this->nodeResolver)();
if (
!is_array($resolved)
|| !isset($resolved[0], $resolved[1])
|| !is_int($resolved[0])
|| !is_int($resolved[1])
) {
throw new \UnexpectedValueException('Snowflake node resolver must return two integer IDs');
}

return $resolved;
return [$resolved[0], $resolved[1]];
}
}
15 changes: 2 additions & 13 deletions src/Configuration/SonyflakeConfig.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@

namespace Infocyph\UID\Configuration;

use Closure;
use DateTimeInterface;
use Infocyph\UID\Enums\ClockBackwardPolicy;
use Infocyph\UID\Enums\IdOutputType;
Expand All @@ -13,11 +12,10 @@
final readonly class SonyflakeConfig
{
use ResolvesCustomEpoch;

private ?Closure $machineIdResolver;
use ResolvesMachineId;

/**
* @param callable():int|null $machineIdResolver
* @param callable():mixed|null $machineIdResolver
*/
public function __construct(
public int $machineId = 0,
Expand All @@ -29,13 +27,4 @@ public function __construct(
) {
$this->machineIdResolver = $machineIdResolver ? $machineIdResolver(...) : null;
}

public function resolveMachineId(): int
{
if ($this->machineIdResolver === null) {
return $this->machineId;
}

return (int) ($this->machineIdResolver)();
}
}
14 changes: 2 additions & 12 deletions src/Configuration/TBSLConfig.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,16 @@

namespace Infocyph\UID\Configuration;

use Closure;
use Infocyph\UID\Enums\ClockBackwardPolicy;
use Infocyph\UID\Enums\IdOutputType;
use Infocyph\UID\Sequence\SequenceProviderInterface;

final readonly class TBSLConfig
{
private ?Closure $machineIdResolver;
use ResolvesMachineId;

/**
* @param callable():int|null $machineIdResolver
* @param callable():mixed|null $machineIdResolver
*/
public function __construct(
public int $machineId = 0,
Expand All @@ -26,13 +25,4 @@ public function __construct(
) {
$this->machineIdResolver = $machineIdResolver ? $machineIdResolver(...) : null;
}

public function resolveMachineId(): int
{
if ($this->machineIdResolver === null) {
return $this->machineId;
}

return (int) ($this->machineIdResolver)();
}
}
Loading