Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
version: 2
updates:
- package-ecosystem: composer
directory: /
schedule:
interval: weekly
open-pull-requests-limit: 5
labels:
- dependencies
commit-message:
prefix: chore
include: scope

- package-ecosystem: github-actions
directory: /
schedule:
interval: weekly
open-pull-requests-limit: 5
labels:
- dependencies
- ci
commit-message:
prefix: chore
include: scope
99 changes: 99 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
name: CI

on:
push:
branches:
- main
- 2.x
pull_request:
branches:
- main
- 2.x

permissions:
contents: read

concurrency:
group: ci-${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

jobs:
static-analysis:
name: Static analysis & code style
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4

- name: Set up PHP
uses: shivammathur/setup-php@v2
with:
php-version: '8.1'
extensions: sockets, openssl
coverage: none
tools: composer:v2

- name: Get composer cache directory
id: composer-cache
run: echo "dir=$(composer config cache-files-dir)" >> "$GITHUB_OUTPUT"

- name: Cache composer dependencies
uses: actions/cache@v4
with:
path: ${{ steps.composer-cache.outputs.dir }}
key: composer-${{ runner.os }}-${{ hashFiles('composer.json') }}
restore-keys: composer-${{ runner.os }}-

- name: Install dependencies
run: composer install --no-interaction --no-progress --prefer-dist

- name: PHPStan
run: composer stan -- --no-progress

- name: PHP-CS-Fixer (dry-run)
run: composer cs-check

tests:
name: Tests (PHP ${{ matrix.php }})
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
php: ['8.1', '8.2', '8.3']
steps:
- name: Checkout
uses: actions/checkout@v4

- name: Set up PHP
uses: shivammathur/setup-php@v2
with:
php-version: ${{ matrix.php }}
extensions: sockets, openssl, pcntl, posix
coverage: pcov
tools: composer:v2

- name: Get composer cache directory
id: composer-cache
run: echo "dir=$(composer config cache-files-dir)" >> "$GITHUB_OUTPUT"

- name: Cache composer dependencies
uses: actions/cache@v4
with:
path: ${{ steps.composer-cache.outputs.dir }}
key: composer-${{ runner.os }}-php${{ matrix.php }}-${{ hashFiles('composer.json') }}
restore-keys: composer-${{ runner.os }}-php${{ matrix.php }}-

- name: Install dependencies
run: composer install --no-interaction --no-progress --prefer-dist

- name: Run test suite
run: composer test-coverage

- name: Upload coverage to Codecov
if: matrix.php == '8.3'
uses: codecov/codecov-action@v4
with:
files: build/coverage.xml
fail_ci_if_error: false
env:
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
7 changes: 6 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,9 @@
/.vs/
/.vscode/
/vendor/
/composer.lock
/composer.lock
/build/
/coverage/
/.phpunit.cache/
/.phpstan-cache/
/.php-cs-fixer.cache
37 changes: 37 additions & 0 deletions .php-cs-fixer.dist.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
<?php

declare(strict_types=1);

$finder = PhpCsFixer\Finder::create()
->in([__DIR__ . '/src', __DIR__ . '/tests'])
->name('*.php')
->ignoreDotFiles(true)
->ignoreVCS(true);

return (new PhpCsFixer\Config())
->setRiskyAllowed(true)
->setRules([
'@PSR12' => true,
'@PSR12:risky' => true,
'@PHP81Migration' => true,
'declare_strict_types' => true,
'native_function_invocation' => [
'include' => ['@compiler_optimized'],
'scope' => 'namespaced',
'strict' => true,
],
'no_unused_imports' => true,
'ordered_imports' => [
'sort_algorithm' => 'alpha',
'imports_order' => ['class', 'function', 'const'],
],
'single_quote' => true,
'array_syntax' => ['syntax' => 'short'],
'trailing_comma_in_multiline' => ['elements' => ['arrays', 'arguments', 'parameters']],
'no_superfluous_phpdoc_tags' => ['allow_mixed' => true],
'phpdoc_align' => ['align' => 'left'],
'phpdoc_order' => true,
'phpdoc_separation' => true,
])
->setFinder($finder)
->setCacheFile(__DIR__ . '/.php-cs-fixer.cache');
118 changes: 118 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
# Changelog

All notable changes to this project will be documented in this file.

The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/)
and this project adheres to [Semantic Versioning 2.0.0](https://semver.org/).

## [Unreleased]

## [2.0.0] β€” TBD

### Highlights

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.

### Added

- **PHP 8.1+ enums** β€” `Transport`, `Domain` and `CryptoMethod` replace
magic integer / string flags.
- **`ChannelInterface` + `TcpChannel` / `UdpChannel` / `StreamChannel`** β€”
per-transport I/O strategy. `ServerConnection` is 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 the `live()`
loop.
- **`register(int|string, SocketConnectionInterface): bool`** β€” promoted
to `SocketServerInterface`; the 1.x package-private `clientRegister()`
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.

### Changed

- **Minimum PHP version** is now `^8.1` (was `>=7.4`).
- **`composer.json`** now requires `ext-openssl` in addition to
`ext-sockets`.
- **Server method renames** β€” `connection()` β†’ `listen()`,
`disconnect()` β†’ `close()`. `live()` signature now takes
`float $idleSeconds` instead of `int $usleep`. `wait()` is typed
`float $seconds`.
- **`SocketServerClientInterface` renamed to `SocketConnectionInterface`.**
`push()` is now `write()`. `isDisconnected()` is replaced by the
non-destructive `isAlive()`.
- **Exception hierarchy** β€” `SocketException` extends
`\RuntimeException`. `SocketConnectionException` and
`SocketListenException` now extend `SocketException` (previously
`\Exception`). `SocketInvalidArgumentException` still extends
`\InvalidArgumentException` and additionally implements
`SocketExceptionInterface`.

### Removed

- `Common/BaseClient`, `Common/BaseCommon`, `Common/BaseServer`,
`Common/ServerTrait`, `Common/StreamClientTrait`,
`Common/StreamServerTrait` β€” replaced by `Client/AbstractClient`,
`Client/AbstractStreamClient`, `Server/AbstractServer` and
`Server/AbstractStreamServer`.
- `Server/ServerClient` β€” replaced by `Server/ServerConnection`.
- `Interfaces/SocketServerClientInterface` β€” replaced by
`Interfaces/SocketConnectionInterface`.
- `Socket::TCP`, `Socket::UDP`, `Socket::TLS`, `Socket::SSL` integer
constants β€” replaced by the `Transport` enum.
- The mystery-typed `$argument` parameter on the factory and on every
constructor β€” replaced by explicit `?Domain $domain` / `?float $timeout`
named parameters.
- All `__setSocket()` / `__setCallbacks()` / `__removeCallbacks()`
magic-prefixed methods.
- The static `ServerClient::$credentials` array that leaked transport
state between server instances.
- `echo` calls inside `ServerClient` that wrote
`"New client connected."` / `"Client disconnected."` to STDOUT.

### Fixed

- **Server loop accepts more than one client.** The 1.x `connection()`
performed a blocking `socket_accept()` before `live()` 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 `clientMap` stored
raw array indices that became dangling after `unset()` on a
disconnected client. The new `clientIdMap` uses 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 via
`stream_select`, giving the handshake the full timeout.

### Migration

See [`docs/migration-1.x-to-2.x.md`](./docs/migration-1.x-to-2.x.md) for
a step-by-step upgrade guide.

[Unreleased]: https://github.com/InitPHP/Socket/compare/v2.0.0...HEAD
[2.0.0]: https://github.com/InitPHP/Socket/releases/tag/v2.0.0
Loading
Loading