bmilleare/sonyflake-php

A faithful PHP port of Sony's Sonyflake distributed unique-ID generator (v2).

Maintainers

Package info

github.com/bmilleare/sonyflake-php

pkg:composer/bmilleare/sonyflake-php

Statistics

Installs: 0

Dependents: 0

Suggesters: 0

Stars: 0

Open Issues: 0

v0.1.0 2026-05-28 12:17 UTC

This package is auto-updated.

Last update: 2026-05-29 18:29:26 UTC


README

A faithful PHP port of Sony's Sonyflake v2 distributed unique-ID generator. Same algorithm, same defaults, same bit layout — produces byte-identical IDs to the Go reference for the same inputs.

CI Packagist PHP Version License

What is Sonyflake?

Sonyflake is a Snowflake-family distributed ID generator that produces sortable 63-bit integer IDs. Its layout differs from Twitter's Snowflake to favour more machines over higher per-machine throughput, and to extend the lifetime to ~174 years:

[ time : 63 − BitsSequence − BitsMachineID bits ]
[ sequence : BitsSequence bits (default 8) ]
[ machine  : BitsMachineID bits (default 16) ]

Defaults match upstream v2: 39 bits of 10ms-resolution time, 8 bits of sequence (256 IDs per 10ms per generator), 16 bits of machine id (65 536 machines), start epoch 2025-01-01T00:00:00Z.

Install

composer require bmilleare/sonyflake-php

Requires PHP ^8.2.

Quick start

use Bmilleare\Sonyflake\Settings;
use Bmilleare\Sonyflake\Sonyflake;

$sf = new Sonyflake(new Settings(machineId: 1));

$id = $sf->nextId();           // e.g. 70368744177665
$parts = $sf->decompose($id);  // Decomposed { id, time, sequence, machine }

Two runnable example scripts ship in examples/:

php examples/basic.php    # explicit machine id, default v2 layout
php examples/leased.php   # two simulated FPM workers leasing distinct slots

⚠️ Machine IDs under PHP-FPM

Sonyflake has no PID or entropy component — uniqueness across processes rides entirely on each generator using a distinct 16-bit machine id. The Go-parity default (PrivateIpResolver, lower 16 bits of the host's private IPv4) gives every PHP-FPM worker on the same host the same id and will collide.

Under FPM (or any setup with multiple worker processes per host), use the shipped LeasedResolver with FileLeaseStore:

use Bmilleare\Sonyflake\MachineId\FileLeaseStore;
use Bmilleare\Sonyflake\MachineId\LeasedResolver;
use Bmilleare\Sonyflake\Settings;
use Bmilleare\Sonyflake\Sonyflake;

$resolver = new LeasedResolver(new FileLeaseStore('/var/run/sonyflake'));
$sf = new Sonyflake(new Settings(machineId: $resolver));

Each worker leases a distinct slot at boot, holds it for its lifetime (default TTL 1 hour, plus a live-PID check), and releases on exit via a shutdown hook.

Resolvers

Resolver Use when
PrivateIpResolver (implicit default) One generator per host. Not safe for FPM.
EnvResolver Orchestrator assigns each container/worker an env var (SONYFLAKE_MACHINE_ID).
LeasedResolver + FileLeaseStore Multiple workers per host (PHP-FPM, Octane, queue workers).

You can implement your own LeaseStore — Redis, APCu, etcd — by providing the three-method interface (acquire, renew, release):

use Bmilleare\Sonyflake\MachineId\Lease;
use Bmilleare\Sonyflake\MachineId\LeaseStore;

final class RedisLeaseStore implements LeaseStore
{
    public function acquire(int $maxId): Lease { /* atomic INCR with TTL */ }
    public function renew(Lease $lease): void { /* refresh TTL */ }
    public function release(Lease $lease): void { /* DEL */ }
}

Configuration

new Settings(
    startTime: new DateTimeImmutable('2025-01-01T00:00:00Z'),
    bitsSequence: 8,                       // 0..30
    bitsMachineId: 16,                     // 0..30; time = 63 − seq − machine ≥ 32
    timeUnitNanos: 10_000_000,             // 10 ms; min 1 ms
    machineId: $resolver,                  // int | MachineIdResolver | null (null → PrivateIpResolver)
    checkMachineId: fn (int $id) => true,  // optional validation
    clock: null,                           // null → SystemClock; inject for tests
);

Exceptions

All exceptions implement Bmilleare\Sonyflake\Exception\SonyflakeException:

Class When
InvalidSettingsException Bad bit widths, time unit, or start time in the future.
InvalidMachineIdException Resolved machine id is out of range or rejected by checkMachineId.
OverTimeLimitException Time bits exhausted (default config: ~174 years from start).
NoPrivateAddressException PrivateIpResolver found no private IPv4.
MachineIdExhaustedException Every lease slot is held by a live worker.

Go-parity notes

  • Sonyflake::nextId() returns int (PHP signed 64-bit). The upstream Go v2 returns int64. Since only 63 bits are used, the value is always non-negative.
  • Sonyflake::decompose() returns a typed Decomposed value object whose toArray() matches Go's map keys: id, time, sequence, machine.
  • Default StartTime is 2025-01-01T00:00:00Z (v2 default — v1 used 2014).
  • Upstream's sf.sequence is initialised to the sequence mask (default 255), so the first NextID() call always wraps it to 0 in the else branch and bumps elapsedTime once. This port reproduces that behaviour exactly.
  • Error sentinels (ErrInvalidBitsTime, ErrOverTimeLimit, …) map to the exception table above; see InvalidSettingsException::* named constructors for the granular settings cases.

Upstream parity verification

Parity isn't just claimed — it's tested against real IDs produced by the Go upstream. tests/Parity/upstream/main.go is a small program that imports github.com/sony/sonyflake/v2, runs NextID() across five configurations (default 8/16 layout, custom 4/8 layout, wider 8/20 layout, alternate machine ids, and a 1 ms time unit), and writes each id together with upstream's Decompose() output to a JSON fixture at tests/Parity/upstream-vectors.json.

UpstreamParityTest.php loads that fixture and asserts the PHP port's Sonyflake::decompose() returns the same (time, sequence, machine) tuples upstream produced for the same ids and Settings. The fixture is checked in, so CI does not need a Go toolchain.

To regenerate against a newer Sonyflake release:

cd tests/Parity/upstream
go run . > ../upstream-vectors.json
composer test

If composer test still passes after regenerating, the port is in sync with that release.

Testing your own code

Inject a Bmilleare\Sonyflake\Clock\Clock implementation that returns deterministic times and records sleeps. The package's own test suite uses exactly this pattern (tests/Support/FakeClock.php).

use Bmilleare\Sonyflake\Clock\Clock;

final class FrozenClock implements Clock
{
    public function __construct(private int $now) {}
    public function nowNanos(): int { return $this->now; }
    public function sleepNanos(int $nanos): void { $this->now += max(0, $nanos); }
}

License

MIT. See LICENSE.

Credits

Faithful port of sony/sonyflake v2 by Sony Group Corporation.