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.
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.
composer require bmilleare/sonyflake-phpRequires PHP ^8.2.
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 slotsSonyflake 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.
| 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 */ }
}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
);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. |
Sonyflake::nextId()returnsint(PHP signed 64-bit). The upstream Go v2 returnsint64. Since only 63 bits are used, the value is always non-negative.Sonyflake::decompose()returns a typedDecomposedvalue object whosetoArray()matches Go's map keys:id,time,sequence,machine.- Default
StartTimeis 2025-01-01T00:00:00Z (v2 default — v1 used 2014). - Upstream's
sf.sequenceis initialised to the sequence mask (default 255), so the firstNextID()call always wraps it to 0 in the else branch and bumpselapsedTimeonce. This port reproduces that behaviour exactly. - Error sentinels (
ErrInvalidBitsTime,ErrOverTimeLimit, …) map to the exception table above; seeInvalidSettingsException::*named constructors for the granular settings cases.
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 testIf composer test still passes after regenerating, the port is in sync
with that release.
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); }
}MIT. See LICENSE.
Faithful port of sony/sonyflake v2 by Sony Group Corporation.