All notable changes to this project will be documented in this file.
The format is based on Keep a Changelog and this project adheres to Semantic Versioning 2.0.0.
2.0.0 — TBD
The 2.0 line is a clean break from 1.x — the previous server loop had
race conditions, lost inbound data through its liveness check, and
stored transport state on a static property that leaked between
instances. The new shape ships with explicit enums, per-transport
Channel strategies, a non-blocking select-driven loop and full
PHP 8.1+ typing.
- PHP 8.1+ enums —
Transport,DomainandCryptoMethodreplace magic integer / string flags. ChannelInterface+TcpChannel/UdpChannel/StreamChannel— per-transport I/O strategy.ServerConnectionis now just identity plus delegation.SocketExceptionInterface— marker implemented by every exception in the package, so a single catch covers them all.tick(callable, float): int— single-iteration accept/dispatch method on every server. Use it to embed the package in your own event loop or to drive servers deterministically in tests.stop()/isRunning()— cooperative shutdown for thelive()loop.register(int|string, SocketConnectionInterface): bool— promoted toSocketServerInterface; the 1.x package-privateclientRegister()is replaced by an interface method with a stable contract.- PHPUnit 10 test suite — 36 unit + integration tests covering enums, exception hierarchy, factory, channels, broadcast/register logic, TCP echo, UDP per-peer routing, TLS handshake (forked).
- CI pipeline — GitHub Actions matrix across PHP 8.1, 8.2, 8.3 with PHPStan level 8, PHP-CS-Fixer and Codecov upload.
docs/directory — getting started, architecture, per-transport server and client guides, cookbook (chat server, raw SMTP) and the migration guide.
- Minimum PHP version is now
^8.1(was>=7.4). composer.jsonnow requiresext-opensslin addition toext-sockets.- Server method renames —
connection()→listen(),disconnect()→close().live()signature now takesfloat $idleSecondsinstead ofint $usleep.wait()is typedfloat $seconds. SocketServerClientInterfacerenamed toSocketConnectionInterface.push()is nowwrite().isDisconnected()is replaced by the non-destructiveisAlive().- Exception hierarchy —
SocketExceptionextends\RuntimeException.SocketConnectionExceptionandSocketListenExceptionnow extendSocketException(previously\Exception).SocketInvalidArgumentExceptionstill extends\InvalidArgumentExceptionand additionally implementsSocketExceptionInterface.
Common/BaseClient,Common/BaseCommon,Common/BaseServer,Common/ServerTrait,Common/StreamClientTrait,Common/StreamServerTrait— replaced byClient/AbstractClient,Client/AbstractStreamClient,Server/AbstractServerandServer/AbstractStreamServer.Server/ServerClient— replaced byServer/ServerConnection.Interfaces/SocketServerClientInterface— replaced byInterfaces/SocketConnectionInterface.Socket::TCP,Socket::UDP,Socket::TLS,Socket::SSLinteger constants — replaced by theTransportenum.- The mystery-typed
$argumentparameter on the factory and on every constructor — replaced by explicit?Domain $domain/?float $timeoutnamed parameters. - All
__setSocket()/__setCallbacks()/__removeCallbacks()magic-prefixed methods. - The static
ServerClient::$credentialsarray that leaked transport state between server instances. echocalls insideServerClientthat wrote"New client connected."/"Client disconnected."to STDOUT.
- Server loop accepts more than one client. The 1.x
connection()performed a blockingsocket_accept()beforelive()was even called, capping the server at a single connection per process. - TLS / SSL servers use the right accept call. The 1.x loop called
socket_accept()on a stream resource (always wrong for TLS / SSL) and never accepted the second connection. - Liveness checks no longer consume data. The 1.x
isDisconnected()read a line off every client every iteration and discarded it, so application-level reads never saw any data. - Client id map survives disconnects. The 1.x
clientMapstored raw array indices that became dangling afterunset()on a disconnected client. The newclientIdMapuses monotonic keys and cleans up on eviction. - UDP server demultiplexes peers correctly. The 1.x server registered the listening socket itself as a "client" and broadcast back to its own host/port.
- TLS handshake has enough time. The 1.x server left the listening
stream non-blocking and called
stream_socket_accept(..., 0.0), which prevented the handshake from completing under load. The new loop keeps the listening stream blocking and drives readiness viastream_select, giving the handshake the full timeout.
See docs/migration-1.x-to-2.x.md for
a step-by-step upgrade guide.