PHPackages                             fallen-komrades-dev/ksuid - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [Utility &amp; Helpers](/categories/utility)
4. /
5. fallen-komrades-dev/ksuid

ActiveLibrary[Utility &amp; Helpers](/categories/utility)

fallen-komrades-dev/ksuid
=========================

K-Sortable Unique Identifier (KSUID) for PHP, ported from ksuid-go and based on tuupola/ksuid

v1.0.0(4d ago)01MITPHP &gt;=8.1

Since Jul 7Compare

[ Source](https://github.com/Fallen-Komrades-Dev/ksuid-php)[ Packagist](https://packagist.org/packages/fallen-komrades-dev/ksuid)[ RSS](/packages/fallen-komrades-dev-ksuid/feed)WikiDiscussions Synced today

READMEChangelog (1)Dependencies (3)Versions (1)Used By (0)

KSUID (PHP)
===========

[](#ksuid-php)

A PHP port of `ksuid-go` — a K-Sortable Unique Identifier library with customizable partitioning and pluggable encoding. Built on the encoder/decoder design from [tuupola/ksuid](https://github.com/tuupola/ksuid), refactored and extended with the full feature set of the Go original: generators, partitioners, and a from-scratch xxHash32 implementation (pure-PHP and GMP-backed).

Features
--------

[](#features)

- **K-Sortable Unique Identifiers** — time-ordered IDs with second precision
- **Customizable partitioning** — distribute IDs across nodes or shards
- **Two generator strategies** — `StandardGenerator` and `AsyncGenerator`, both with the exact overflow-guard semantics of the Go original
- **Pure-PHP and GMP-backed xxHash32** — pick whichever fits your environment; both are verified byte-for-byte against the canonical 256-vector xxHash32 reference table
- **Strict, typed value object** — `Ksuid` is an immutable, typed, `JsonSerializable`, `Stringable` PHP 8.1+ class
- **Comprehensive test suite** — 1,600+ PHPUnit tests, including exact fixture parity with the Go test suite

Installation
------------

[](#installation)

```
composer require fallen-komrades-dev/ksuid
```

Requires PHP 8.1 or later. The `ext-gmp` extension is optional but recommended if you want the GMP-backed xxHash32 implementation (`XXHash32Gmp`) — the pure-PHP implementation (`XXHash32`) has no extension dependencies and is used internally by the rest of the library by default.

Quick Start
-----------

[](#quick-start)

```
use FallenKomradesDev\KSUID\Generator\StandardGenerator;

$generator = new StandardGenerator();

$k = $generator->next();
echo $k->toString();      // e.g. "31c8d0a9000000000000000060902cf5"
echo $k->time()->format(DATE_ATOM); // when it was generated
echo $k->seq;              // sequence number within that second
echo $k->partition;        // partition (0 by default)
```

Unlike some KSUID libraries that expose a package-level singleton (`ksuid.Next()`, `ksuid.SetPartitioner()`), this package has you construct and hold a generator instance directly. This avoids hidden global mutable state and makes dependency injection straightforward.

Usage
-----

[](#usage)

### Standard Generator

[](#standard-generator)

```
use FallenKomradesDev\KSUID\Generator\StandardGenerator;

$generator = new StandardGenerator();
$k = $generator->next();
```

`StandardGenerator` checks the wall clock on every call to `next()`, advancing its internal timestamp and resetting the sequence counter whenever a new second begins — exactly mirroring Go's `StandardGenerator.Next()`, including its sequence-overflow guard (if you generate more than ~4.29 billion IDs within a single second, the stored timestamp is advanced by one to guarantee uniqueness rather than blocking).

> **Concurrency note:** Go's `StandardGenerator` uses a `sync.Mutex` because goroutines can call `Next()` from multiple OS threads simultaneously. Standard PHP request handling (`php-fpm`, `mod_php`, CLI scripts) has exactly one thread of execution per request, so there's no analogous data race to guard against. If you're running this in a genuinely multi-threaded PHP runtime (pthreads, parallel, Swoole/RoadRunner workers sharing memory), you're responsible for serializing calls to `next()` yourself.

### Async Generator

[](#async-generator)

```
use FallenKomradesDev\KSUID\Generator\AsyncGenerator;

$generator = new AsyncGenerator();

// Convenience method: checks the clock and generates in one call.
$k = $generator->autoNext();
```

Go's `AsyncGenerator` runs a background goroutine that refreshes a cached timestamp once per second via a `time.Ticker`, while `Next()` does a lock-free atomic increment — a design specifically built to avoid mutex contention across many goroutines on many cores. PHP has no goroutines or background threads in the typical request-per-process model, so this class preserves the same *external* lifecycle shape (`run()` / `stop()` / `tick()`) and, critically, the same *sequencing semantics* — including the fix for a historical bug where the sequence counter must only reset when the timestamp has genuinely advanced, never merely because a timer fired.

Two ways to use it:

```
// Style 1 — explicit ticking, for parity with environments that do have
// a real event loop (Swoole, ReactPHP, RoadRunner): drive tick() yourself
// from a periodic timer, then call next() (which never touches the clock).
$generator = new AsyncGenerator();
$generator->tick();              // call this roughly once a second
$k = $generator->next();         // pure increment, no clock access

// Style 2 — polling convenience (PHP-specific, no Go equivalent): checks
// the clock and generates in a single call, for plain scripts with no
// separate event loop.
$generator = new AsyncGenerator();
$k = $generator->autoNext();
```

`run()` and `stop()` are provided for API-shape parity with Go (they simply flip an `isRunning()` flag, since there's no background thread to literally start/stop).

### Custom Partitioning

[](#custom-partitioning)

```
use FallenKomradesDev\KSUID\Generator\StandardGenerator;
use FallenKomradesDev\KSUID\Partition\StringPartitioner;

$generator = new StandardGenerator(new StringPartitioner('service-a'));
$k = $generator->next();
```

Both generators accept any `PartitionerInterface` implementation **or** a plain `callable`/`Closure` returning an `int` — no interface implementation is required for simple cases:

```
$counter = 0;
$generator = new StandardGenerator(function () use (&$counter): int {
    return $counter++;
});
```

Partitioners
------------

[](#partitioners)

Partitioners split KSUIDs into logical partitions, useful in distributed systems where each node or shard should write to a distinguishable partition.

ClassDescription`NilPartitioner`Always returns `0` (the default if no partitioner is supplied)`StringPartitioner`Hashes the full input string with xxHash32, avoiding prefix collisions that a naive truncation scheme would suffer (e.g. `"user-123"` and `"user-456"` hash to different partitions despite sharing a prefix)`MacPartitioner`Uses the last 4 bytes of the host's primary network interface MAC address; throws `NoNetworkInterfaceException` if none can be found```
use FallenKomradesDev\KSUID\Partition\MacPartitioner;
use FallenKomradesDev\KSUID\Partition\StringPartitioner;
use FallenKomradesDev\KSUID\Exception\NoNetworkInterfaceException;

try {
    $partitioner = new MacPartitioner();
} catch (NoNetworkInterfaceException $e) {
    $partitioner = new StringPartitioner('fallback-shard');
}
```

`MacPartitioner` has no PHP standard-library equivalent to Go's `net.Interfaces()`, so it discovers the MAC address by platform-appropriate means: reading `/sys/class/net/*/address` on Linux, shelling out to `getmac` on Windows, and `ifconfig` on macOS. This is inherently environment-dependent — minimal containers or sandboxes may have no non-loopback interface at all, which is why the test suite tolerates either a valid result or the documented exception rather than asserting a hardcoded value.

Encoding &amp; Decoding
-----------------------

[](#encoding--decoding)

```
use FallenKomradesDev\KSUID\Encoder;

$k = $generator->next();

// Hex encoding (32 characters)
$hex = $k->toString(); // or (string) $k
$decoded = Encoder::decodeHex($hex);

// Binary encoding (16 bytes)
$binary = $k->binary();
$decoded = Encoder::decodeBinary($binary);
```

`decodeHex()`/`decodeBinary()` throw on malformed input:

```
use FallenKomradesDev\KSUID\Exception\InvalidLengthException;
use FallenKomradesDev\KSUID\Exception\InvalidHexLengthException;
use FallenKomradesDev\KSUID\Exception\InvalidHexCharacterException;
use FallenKomradesDev\KSUID\Exception\HashMismatchException;

try {
    Encoder::decodeHex($untrustedInput);
} catch (InvalidHexLengthException $e) {
    // not exactly 32 characters
} catch (InvalidHexCharacterException $e) {
    // contains non-hex characters
} catch (HashMismatchException $e) {
    // the embedded hash doesn't match the decoded fields — corrupted or tampered
}
```

### JSON

[](#json)

`Ksuid` implements `JsonSerializable`, encoding as its 32-character hex string:

```
$k = $generator->next();
echo json_encode(['id' => $k]); // {"id":"31c8d0a9..."}

$decoded = \FallenKomradesDev\KSUID\Ksuid::fromJson('"31c8d0a9..."');
```

Comparing KSUIDs
----------------

[](#comparing-ksuids)

```
$cmp = $k1->compare($k2);
// -1 if $k1 < $k2, 0 if equal, +1 if $k1 > $k2
// Ordered by: timestamp -> partition -> sequence

$k1->equals($k2); // strict equality across every field, including Hash
```

KSUID Structure
---------------

[](#ksuid-structure)

```
[Timestamp:4 bytes][Seq:4 bytes][Partition:4 bytes][Hash:4 bytes]

```

16 bytes (128 bits) total, big-endian. The `Hash` field is the xxHash32 checksum of the preceding 12 bytes, computed automatically and verified on every decode.

- **Timestamp** (uint32) — seconds since `Ksuid::getEpoch()` (defaults to `2000-01-01T00:00:00Z`, overridable via `Ksuid::setEpoch()`)
- **Seq** (uint32) — monotonic counter within the same second
- **Partition** (uint32) — identifies the generating node/partition
- **Hash** (uint32) — xxHash32 integrity checksum

xxHash32
--------

[](#xxhash32)

This package includes a from-scratch xxHash32 implementation (seed = 0), since KSUID's integrity hash depends on it and PHP has no built-in xxHash support.

```
use FallenKomradesDev\KSUID\XXHash32\XXHash32;
use FallenKomradesDev\KSUID\XXHash32\XXHash32Gmp;

// One-shot
$hash = XXHash32::checksumZero('hello world');

// Streaming
$hasher = new XXHash32();
$hasher->reset();
$hasher->write('hello ');
$hasher->write('world');
$hash = $hasher->sum32();

// GMP-backed equivalent, identical API
$hash = XXHash32Gmp::checksumZero('hello world');
```

Both implementations are validated against the canonical 256-entry xxHash32 reference vector table (hashing byte sequences `[0]`, `[0,1]`, `[0,1,2]`, ... up to length 255) and cross-checked against each other for agreement on randomized and irregularly-chunked input. The pure-PHP version handles PHP's lack of native 32-bit unsigned integer overflow by splitting multiplications into 16-bit halves so every intermediate value stays within 64-bit signed integer range without ever silently promoting to float — a real, easy-to-miss pitfall when porting C/Go-style unsigned 32-bit hash algorithms to PHP.

Testing
-------

[](#testing)

```
composer test              # run the full PHPUnit suite
composer test-coverage     # run with HTML + text coverage report
composer bench             # run the benchmark suite
```

Or run the verification pipeline directly (lint, tests, benchmarks, coverage — see [`verify.sh`](verify.sh) / [`verify.bat`](verify.bat)):

```
./verify.sh     # Linux/macOS/WSL
verify.bat      # Windows
```

The test suite includes:

- All 256 canonical xxHash32 reference vectors, against both the pure-PHP and GMP implementations, via one-shot and multiple streaming chunk patterns (including stress tests with irregular, non-uniform write sizes far beyond what the Go test suite covers)
- Exact fixture parity with `ksuid-go`'s `ksuid_test.go`/`encoder_test.go`/`partition_test.go` — the same `Timestamp`/`Seq`/`Partition`/`Hash` values, the same expected hex string, the same expected `Time()` output
- Sequence-overflow edge cases for both generators, including deterministic tests that bypass the live wall-clock check to genuinely (not just incidentally) exercise the overflow guard
- A dedicated regression test for the historical "ticker reset" bug in `AsyncGenerator`, verifying `next()` never touches the wall clock on its own

Project Layout
--------------

[](#project-layout)

```
src/
  Ksuid.php                       Immutable value object
  Encoder.php                     Binary/hex codec
  Exception/                      Typed exceptions (one per Go sentinel error)
  Partition/
    PartitionerInterface.php
    NilPartitioner.php
    StringPartitioner.php
    MacPartitioner.php
  Generator/
    GeneratorInterface.php
    StandardGenerator.php
    AsyncGenerator.php
  XXHash32/
    XXHash32.php                  Pure-PHP implementation
    XXHash32Gmp.php                GMP-backed implementation
tests/                            PHPUnit test suite, mirroring src/ structure
examples/main.php                 Runnable usage examples
benchmark.php                     Micro-benchmark harness
verify.sh / verify.bat            Full verification pipeline

```

Differences from the Go original
--------------------------------

[](#differences-from-the-go-original)

- No package-level singleton generator/partitioner (`ksuid.Next()`, `ksuid.SetPartitioner()`) — construct and hold a generator instance instead.
- `AsyncGenerator` adapts Go's goroutine-and-ticker design to PHP's execution model: `next()` never touches the wall clock (exactly like Go's `Next()`), `tick()` is the explicit timer-driven refresh, and `autoNext()` is a PHP-specific convenience that combines both for simple polling use.
- PHP has no native unsigned 32-bit integer type; all four `Ksuid` fields are ordinary PHP `int`s masked to `[0, 0xFFFFFFFF]`, which fits losslessly inside PHP's 64-bit signed integers.
- `MacPartitioner` discovers the MAC address via platform-specific file reads/shell commands rather than a standard-library network API, since PHP has none.

License
-------

[](#license)

BSD 3-Clause

###  Health Score

38

—

LowBetter than 83% of packages

Maintenance99

Actively maintained with recent releases

Popularity2

Limited adoption so far

Community2

Small or concentrated contributor base

Maturity40

Maturing project, gaining track record

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

###  Release Activity

Cadence

Unknown

Total

1

Last Release

4d ago

### Community

Maintainers

![](https://www.gravatar.com/avatar/e1ae748049fbd9489bb6149a04775584347281a479f68b0663528905cabad7e9?d=identicon)[Docwyatt](/maintainers/Docwyatt)

---

Tags

uuididentifiersortableuniqueksuid

###  Code Quality

TestsPHPUnit

Static AnalysisPHPStan

Type Coverage Yes

### Embed Badge

![Health badge](/badges/fallen-komrades-dev-ksuid/health.svg)

```
[![Health](https://phpackages.com/badges/fallen-komrades-dev-ksuid/health.svg)](https://phpackages.com/packages/fallen-komrades-dev-ksuid)
```

###  Alternatives

[ramsey/uuid

A PHP library for generating and working with universally unique identifiers (UUIDs).

12.6k745.0M4.1k](/packages/ramsey-uuid)[pascaldevink/shortuuid

PHP 7.4+ library that generates concise, unambiguous, URL-safe UUIDs

5941.9M16](/packages/pascaldevink-shortuuid)[keiko/uuid-shortener

A simple shortener library for RFC 4122 compatible UUIDs. Change your 36 chars long UUID into it's shorter equivalent.

150227.4k3](/packages/keiko-uuid-shortener)[oittaa/uuid

A small PHP class for generating RFC 9562 universally unique identifiers (UUID) from version 3 to version 8.

54324.8k6](/packages/oittaa-uuid)[ekreative/uuid-extra-bundle

Paramconverter, Normalizer and Form Type for Ramsey Uuid

18170.8k](/packages/ekreative-uuid-extra-bundle)[infocyph/uid

UUID (RFC 9562), ULID, Snowflake ID, Sonyflake ID, and TBSL generator for PHP.

116.0k](/packages/infocyph-uid)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
