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
14 changes: 13 additions & 1 deletion .github/workflows/security-standards.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,22 @@ jobs:
php_versions: '["8.2","8.3","8.4","8.5"]'
dependency_versions: '["prefer-lowest","prefer-stable"]'
php_extensions: "bcmath"
coverage: "xdebug"
composer_flags: ""
phpstan_memory_limit: "1G"
psalm_threads: "1"
run_analysis: true
run_svg_report: true
fail_on_skipped_tests: false
enable_redis_service: false
enable_valkey_service: false
enable_memcached_service: false
enable_postgres_service: false
enable_mysql_service: false
enable_scylladb_service: false
enable_elasticsearch_service: false
enable_mongodb_service: false
service_db_name: "phpforge"
service_db_user: "phpforge"
service_db_password: "phpforge"
artifact_retention_days: 61

8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,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)
- NanoID, CUID2, KSUID, XID
- Opaque and deterministic IDs
- Value objects and comparator utilities
Expand Down Expand Up @@ -48,6 +49,12 @@ $ulid = Id::ulid();
$snowflake = Id::snowflake();
$sonyflake = Id::sonyflake();
$tbsl = Id::tbsl();
$randflake = \Infocyph\UID\Randflake::generate(
nodeId: 42,
leaseStart: time() - 5,
leaseEnd: time() + 300,
secret: 'super-secret-key',
);
$nanoid = NanoID::generate(21);
$cuid2 = CUID2::generate(24);
```
Expand Down Expand Up @@ -77,6 +84,7 @@ The shared byte-level encoder is available as
- ULID: https://github.com/ulid/spec
- Snowflake: https://github.com/twitter-archive/snowflake/tree/snowflake-2010
- Sonyflake: https://github.com/sony/sonyflake
- Randflake: https://github.com/gosuda/randflake
- NanoID: https://github.com/ai/nanoid
- CUID2: https://github.com/paralleldrive/cuid2
- TBSL note: https://github.com/infocyph/UID/blob/main/TBSL.md
32 changes: 32 additions & 0 deletions benchmarks/BenchBootstrap.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
<?php

declare(strict_types=1);

namespace Infocyph\UID\Benchmarks;

final class BenchBootstrap
{
public static function load(): void
{
require_once dirname(__DIR__) . '/vendor/autoload.php';
}

/**
* @return array{0:int,1:int,2:string}
*/
public static function randflakeContext(int $ttlSeconds = 3600): array
{
$now = time();

return [
$now - 5,
$now + $ttlSeconds,
self::randflakeSecret(),
];
}

public static function randflakeSecret(): string
{
return 'super-secret-key';
}
}
231 changes: 221 additions & 10 deletions benchmarks/HotspotBench.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,19 +5,81 @@
namespace Infocyph\UID\Benchmarks;

use DateTimeImmutable;
use Infocyph\UID\CUID2;
use Infocyph\UID\DeterministicId;
use Infocyph\UID\KSUID;
use Infocyph\UID\NanoID;
use Infocyph\UID\OpaqueId;
use Infocyph\UID\Randflake;
use Infocyph\UID\Snowflake;
use Infocyph\UID\Sonyflake;
use Infocyph\UID\TBSL;
use Infocyph\UID\ULID;
use Infocyph\UID\UUID;
use Infocyph\UID\XID;
use InvalidArgumentException;
use PhpBench\Attributes as Bench;

final class HotspotBench
{
private string $cuid2;

private string $ksuid;

private int $leaseEnd;

private int $leaseStart;

private string $nanoid;

private string $randflake;

private string $randflakeSecret;

private string $snowflake;

private string $sonyflake;

private string $tbsl;

private string $ulid;

private string $uuid;

private string $xid;

public function __construct()
{
require_once __DIR__ . '/BenchBootstrap.php';
BenchBootstrap::load();
$this->prepareRandflakeContext();

$this->uuid = UUID::v7();
$this->ulid = ULID::generate();
$this->snowflake = Snowflake::generate();
$this->sonyflake = Sonyflake::generate();
$this->tbsl = TBSL::generate();
$this->ksuid = KSUID::generate();
$this->xid = XID::generate();
$this->nanoid = NanoID::generate();
$this->cuid2 = CUID2::generate();
$this->randflake = Randflake::generate(42, $this->leaseStart, $this->leaseEnd, $this->randflakeSecret);
}

#[Bench\Revs(1000)]
#[Bench\Iterations(5)]
#[Bench\ParamProviders('provideGenerationSubjects')]
public function benchGeneration(array $params): void
{
$this->runSubjectBench('generation', $params);
}

#[Bench\Revs(1000)]
#[Bench\Iterations(5)]
public function benchSnowflakeParse(): void
#[Bench\ParamProviders('provideParseSubjects')]
public function benchParse(array $params): void
{
$id = Snowflake::generate();
Snowflake::parse($id);
$this->runSubjectBench('parse', $params);
}

#[Bench\Revs(1000)]
Expand All @@ -30,16 +92,165 @@ public function benchUlidMonotonicBurstSameMs(): void

#[Bench\Revs(1000)]
#[Bench\Iterations(5)]
public function benchUuid7Generation(): void
#[Bench\ParamProviders('provideValidationSubjects')]
public function benchValidation(array $params): void
{
UUID::v7();
$this->runSubjectBench('validation', $params);
}

#[Bench\Revs(1000)]
#[Bench\Iterations(5)]
public function benchUuidParse(): void
/**
* @return array<string, array{subject: string}>
*/
public function provideGenerationSubjects(): array
{
return [
'cuid2' => ['subject' => 'cuid2'],
'deterministic' => ['subject' => 'deterministic'],
'ksuid' => ['subject' => 'ksuid'],
'nanoid' => ['subject' => 'nanoid'],
'opaque' => ['subject' => 'opaque'],
'randflake' => ['subject' => 'randflake'],
'snowflake' => ['subject' => 'snowflake'],
'sonyflake' => ['subject' => 'sonyflake'],
'tbsl' => ['subject' => 'tbsl'],
'ulid' => ['subject' => 'ulid'],
'uuid_v7' => ['subject' => 'uuid_v7'],
'xid' => ['subject' => 'xid'],
];
}

/**
* @return array<string, array{subject: string}>
*/
public function provideParseSubjects(): array
{
return [
'ksuid' => ['subject' => 'ksuid'],
'randflake_inspect' => ['subject' => 'randflake_inspect'],
'randflake_parse' => ['subject' => 'randflake_parse'],
'snowflake' => ['subject' => 'snowflake'],
'sonyflake' => ['subject' => 'sonyflake'],
'tbsl' => ['subject' => 'tbsl'],
'ulid_get_time' => ['subject' => 'ulid_get_time'],
'uuid' => ['subject' => 'uuid'],
'xid' => ['subject' => 'xid'],
];
}

/**
* @return array<string, array{subject: string}>
*/
public function provideValidationSubjects(): array
{
return [
'cuid2' => ['subject' => 'cuid2'],
'nanoid' => ['subject' => 'nanoid'],
'randflake' => ['subject' => 'randflake'],
'snowflake' => ['subject' => 'snowflake'],
'sonyflake' => ['subject' => 'sonyflake'],
'tbsl' => ['subject' => 'tbsl'],
];
}

private function prepareRandflakeContext(): void
{
[$this->leaseStart, $this->leaseEnd, $this->randflakeSecret] = BenchBootstrap::randflakeContext();
}

/**
* @param array{subject?: mixed} $params
*/
private function runSubjectBench(string $operation, array $params): void
{
$subject = $this->subject($params);
$runner = match ($operation) {
'generation' => [
'cuid2' => fn() => CUID2::generate(),
'deterministic' => fn() => DeterministicId::fromPayload('payload', 24, 'bench'),
'ksuid' => fn() => KSUID::generate(),
'nanoid' => fn() => NanoID::generate(),
'opaque' => fn() => OpaqueId::random(12),
'randflake' => fn() => Randflake::generate(42, $this->leaseStart, $this->leaseEnd, $this->randflakeSecret),
'snowflake' => fn() => Snowflake::generate(),
'sonyflake' => fn() => Sonyflake::generate(),
'tbsl' => fn() => TBSL::generate(),
'ulid' => fn() => ULID::generate(),
'uuid_v7' => fn() => UUID::v7(),
'xid' => fn() => XID::generate(),
],
'parse' => [
'ksuid' => fn() => KSUID::parse($this->ksuid),
'randflake_inspect' => fn() => Randflake::inspect($this->randflake, $this->randflakeSecret),
'randflake_parse' => fn() => Randflake::parse($this->randflake, $this->randflakeSecret),
'snowflake' => fn() => Snowflake::parse($this->snowflake),
'sonyflake' => fn() => Sonyflake::parse($this->sonyflake),
'tbsl' => fn() => TBSL::parse($this->tbsl),
'ulid_get_time' => fn() => ULID::getTime($this->ulid),
'uuid' => fn() => UUID::parse($this->uuid),
'xid' => fn() => XID::parse($this->xid),
],
'validation' => $this->validationRunners(),
default => throw new InvalidArgumentException("Unknown benchmark operation: $operation"),
};
$handler = $runner[$subject] ?? throw new InvalidArgumentException("Unknown {$operation} subject: $subject");
$handler();
}

/**
* @param array{subject?: mixed} $params
*/
private function subject(array $params): string
{
$subject = $params['subject'] ?? null;
if (!is_string($subject) || $subject === '') {
throw new InvalidArgumentException('Benchmark subject is required.');
}

return $subject;
}

private function validateCuid2(): void
{
CUID2::isValid($this->cuid2);
}

private function validateNanoid(): void
{
NanoID::isValid($this->nanoid);
}

private function validateRandflake(): void
{
Randflake::isValid($this->randflake);
}

private function validateSnowflake(): void
{
Snowflake::isValid($this->snowflake);
}

private function validateSonyflake(): void
{
Sonyflake::isValid($this->sonyflake);
}

private function validateTbsl(): void
{
TBSL::isValid($this->tbsl);
}

/**
* @return array<string, callable():void>
*/
private function validationRunners(): array
{
$id = UUID::v7();
UUID::parse($id);
return [
'cuid2' => fn() => $this->validateCuid2(),
'nanoid' => fn() => $this->validateNanoid(),
'randflake' => fn() => $this->validateRandflake(),
'snowflake' => fn() => $this->validateSnowflake(),
'sonyflake' => fn() => $this->validateSonyflake(),
'tbsl' => fn() => $this->validateTbsl(),
];
}
}
Loading