bmilleare / sonyflake-php
A faithful PHP port of Sony's Sonyflake distributed unique-ID generator (v2).
Requires
- php: ^8.2
Requires (Dev)
- laravel/pint: ^1.20
- pestphp/pest: ^3.0
- phpstan/phpstan: ^2.0
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.
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()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.
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.