diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml new file mode 100644 index 0000000..8e74283 --- /dev/null +++ b/.github/workflows/tests.yml @@ -0,0 +1,46 @@ +name: Tests + +on: + push: + branches: + - main + pull_request: + +jobs: + tests: + name: PHP ${{ matrix.php-version }} + runs-on: ubuntu-latest + + strategy: + fail-fast: false + matrix: + php-version: + - '8.2' + - '8.3' + - '8.4' + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php-version }} + extensions: sockets, json, pdo + coverage: none + + - name: Install dependencies + run: composer install --no-interaction --prefer-dist + + - name: Validate Composer package + run: composer validate --strict + + - name: Check code style + run: composer cs:check + + - name: Run static analysis + run: composer analyse + + - name: Run tests + run: composer test \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..776c70d --- /dev/null +++ b/.gitignore @@ -0,0 +1,42 @@ +/vendor/ +/composer.lock + +/.phpunit.cache/ +/.php-cs-fixer.cache +/.phpstan.cache +/coverage/ +/build/ +/dist/ + +.zip +.env +.env.* +!.env.example + +*.log +*.tmp +*.temp +*.cache + +.DS_Store +Thumbs.db +desktop.ini + +.idea/ +.vscode/ +*.sublime-project +*.sublime-workspace + +.phpunit.result.cache + +/storage/*.sqlite +/storage/*.sqlite3 +/storage/*.db + +node_modules/ +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* + +/.phpunit.cache \ No newline at end of file diff --git a/.php-cs-fixer.php b/.php-cs-fixer.php new file mode 100644 index 0000000..ec5670b --- /dev/null +++ b/.php-cs-fixer.php @@ -0,0 +1,18 @@ +in(__DIR__ . '/src') + ->in(__DIR__ . '/tests') + ->name('*.php'); + +return (new PhpCsFixer\Config()) + ->setRiskyAllowed(true) + ->setRules([ + '@PSR12' => true, + 'array_syntax' => ['syntax' => 'short'], + 'declare_strict_types' => true, + 'no_unused_imports' => true, + 'ordered_imports' => ['sort_algorithm' => 'alpha'], + 'single_quote' => true, + ]) + ->setFinder($finder); \ No newline at end of file diff --git a/README.md b/README.md index 3acde32..f660ceb 100644 --- a/README.md +++ b/README.md @@ -1,20 +1,116 @@ # PHPSockets With WebSockets -PHPSockets With WebSockets is being reborn as a modern native PHP WebSocket and realtime chat library. +> **Maintenance notice:** this repository is being actively rebuilt as a modern native PHP WebSocket and realtime chat library. The legacy 2016 implementation is preserved for historical reference, while the new Composer-based library is being implemented phase by phase. -The original project was created in 2016 as a simple educational experiment showing how to build a WebSocket server with PHP sockets, without Node.js and without socket.io. +PHPSockets With WebSockets was originally created in 2016 as an educational experiment showing how to build a WebSocket server with PHP sockets, without Node.js and without socket.io. -This repository is now being progressively redesigned as a Composer package with a clean architecture, modern PHP support, examples, tests, storage adapters, CLI commands, Laravel integration, and a stronger chat-focused developer experience. +The project is now being progressively redesigned as a Composer package with a clean architecture, modern PHP support, examples, tests, storage adapters, CLI commands, optional Laravel integration, and a stronger chat-focused developer experience. ## Current status -This repository is currently in the legacy preservation phase. +The project is currently in the **Composer foundation phase**. -The original 2016 implementation has been moved to the `legacy/` directory so it can be preserved as historical reference while the new library is built from scratch in future phases. +This means the repository already has the initial Composer package structure, PSR-4 namespace configuration, base configuration classes, PHPUnit setup, PHPStan setup, PHP CS Fixer setup, and GitHub Actions workflow. + +The modern WebSocket runtime is not implemented yet. + +## Installation for development + +Clone the repository and install dependencies: + +```bash +composer install +``` + +Validate the Composer package: + +```bash +composer validate --strict +``` + +Run the test suite: + +```bash +composer test +``` + +Run static analysis: + +```bash +composer analyse +``` + +Check code style: + +```bash +composer cs:check +``` + +Run all quality checks: + +```bash +composer quality +``` + +Fix code style automatically: + +```bash +composer cs:fix +``` + +## Requirements + +The modern version targets: + +- PHP 8.2 or higher. +- `ext-sockets`. +- `ext-json`. +- Composer. + +Optional future features may require: + +- `ext-pdo` for SQL storage adapters. +- Laravel packages for optional Laravel integration. + +## Namespace + +The modern library uses the following namespace: + +```txt +Micilini\PhpSockets\ +``` + +The current public entry point is: + +```php +use Micilini\PhpSockets\WebSocket; + +echo WebSocket::version(); +``` + +## Current structure + +```txt +src/ + WebSocket.php + Config/ + ServerConfig.php + ChatConfig.php + +tests/ + Unit/ + SanityTest.php + +legacy/ + EasyChat/ + MediumChat/ + README-2016.md + NOTES.md +``` ## Legacy code -The original code is available here: +The original 2016 implementation is preserved here: ```txt legacy/EasyChat @@ -26,6 +122,8 @@ legacy/README-2016.md `MediumChat` contains the more advanced object-oriented version with callbacks and a better separation between the WebSocket server and the chat behavior. +The legacy implementation is kept for historical and educational purposes only. The modern library will be implemented separately and should not depend on the old code structure. + ## Why the legacy code was moved The old project was designed for PHP 5 and browser-based local testing. At that time, the server could be started by opening `server.php` in the browser or by running it manually. @@ -53,16 +151,36 @@ The new implementation will be developed gradually and will include: - Laravel integration. - Tests, static analysis, and CI. -## Roadmap +## Roadmap phases + +The project is being implemented phase by phase: + +```txt +Phase 00: Legacy preservation. +Phase 01: Composer foundation, namespace, quality tools, and CI. +Phase 02: WebSocket protocol core. +Phase 03: Server runtime and events. +Phase 04: Chat core and unique usernames. +Phase 05: Modern EasyChat example. +Phase 06: MediumChat example with callbacks. +Phase 07: Private direct messaging. +Phase 08: Private group rooms. +Phase 09: Storage adapters and migrations. +Phase 10: CLI runtime commands. +Phase 11: Small attachments and emoji-safe payloads. +Phase 12: Bot hooks and automation events. +Phase 13: Laravel integration. +Phase 14: Release documentation and Packagist preparation. +``` -Implementation will follow the project roadmap phase by phase. +## Production readiness -The first phase preserves the legacy code and creates a clean baseline. +This package is not production-ready yet. -Future phases will introduce Composer, source code structure, WebSocket protocol handling, server runtime, chat features, examples, persistence, CLI tooling, and release documentation. +The repository is currently being rebuilt. The modern WebSocket protocol, server runtime, chat system, examples, storage adapters, CLI commands, and Laravel integration will be added in future phases. ## Important note -The legacy implementation is kept for historical and educational purposes. +The goal is not only to restore an old chat demo. -The new library will be implemented separately and should not depend on the old code structure. +The goal is to transform PHPSockets With WebSockets into a modern, educational, extensible, native PHP realtime library. diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..0dbc102 --- /dev/null +++ b/composer.json @@ -0,0 +1,51 @@ +{ + "name": "micilini/php-sockets-with-websockets", + "description": "Native PHP WebSocket and realtime chat library built from scratch. Works standalone or with Laravel.", + "type": "library", + "license": "MIT", + "keywords": [ + "websocket", + "sockets", + "chat", + "realtime", + "php", + "laravel" + ], + "require": { + "php": "^8.2", + "ext-sockets": "*", + "ext-json": "*" + }, + "require-dev": { + "phpunit/phpunit": "^10.0|^11.0", + "phpstan/phpstan": "^1.10|^2.0", + "friendsofphp/php-cs-fixer": "^3.0" + }, + "suggest": { + "ext-pdo": "Required for SQL storage adapters and migrations.", + "illuminate/support": "Required for Laravel integration." + }, + "autoload": { + "psr-4": { + "Micilini\\PhpSockets\\": "src/" + } + }, + "autoload-dev": { + "psr-4": { + "Micilini\\PhpSockets\\Tests\\": "tests/" + } + }, + "scripts": { + "test": "phpunit", + "analyse": "phpstan analyse src tests --memory-limit=512M", + "cs:check": "php-cs-fixer fix --dry-run --diff", + "cs:fix": "php-cs-fixer fix", + "quality": [ + "@cs:check", + "@analyse", + "@test" + ] + }, + "minimum-stability": "stable", + "prefer-stable": true +} \ No newline at end of file diff --git a/phpstan.neon b/phpstan.neon new file mode 100644 index 0000000..4ba9d80 --- /dev/null +++ b/phpstan.neon @@ -0,0 +1,5 @@ +parameters: + level: 6 + paths: + - src + - tests \ No newline at end of file diff --git a/phpunit.xml b/phpunit.xml new file mode 100644 index 0000000..45a87ab --- /dev/null +++ b/phpunit.xml @@ -0,0 +1,17 @@ + + + + + tests + + + + + src + + + \ No newline at end of file diff --git a/src/Config/ChatConfig.php b/src/Config/ChatConfig.php new file mode 100644 index 0000000..2b774d6 --- /dev/null +++ b/src/Config/ChatConfig.php @@ -0,0 +1,50 @@ +maxDisplayNameLength < 1) { + throw new InvalidArgumentException('Maximum display name length must be greater than zero.'); + } + + if ($this->maxRoomNameLength < 1) { + throw new InvalidArgumentException('Maximum room name length must be greater than zero.'); + } + + if ($this->maxPrivateGroupMembers < 2) { + throw new InvalidArgumentException('Maximum private group members must be at least 2.'); + } + + if ($this->historyLimit < 0) { + throw new InvalidArgumentException('History limit cannot be negative.'); + } + } + + public static function new( + int $maxDisplayNameLength = 40, + int $maxRoomNameLength = 80, + int $maxPrivateGroupMembers = 20, + bool $allowGuestSessions = true, + int $historyLimit = 50, + ): self { + return new self( + maxDisplayNameLength: $maxDisplayNameLength, + maxRoomNameLength: $maxRoomNameLength, + maxPrivateGroupMembers: $maxPrivateGroupMembers, + allowGuestSessions: $allowGuestSessions, + historyLimit: $historyLimit, + ); + } +} diff --git a/src/Config/ServerConfig.php b/src/Config/ServerConfig.php new file mode 100644 index 0000000..574b5a8 --- /dev/null +++ b/src/Config/ServerConfig.php @@ -0,0 +1,57 @@ +host === '') { + throw new InvalidArgumentException('Server host cannot be empty.'); + } + + if ($this->port < 1 || $this->port > 65535) { + throw new InvalidArgumentException('Server port must be between 1 and 65535.'); + } + + if ($this->maxPayloadBytes < 1) { + throw new InvalidArgumentException('Maximum payload size must be greater than zero.'); + } + + if ($this->tickMicroseconds < 1) { + throw new InvalidArgumentException('Tick interval must be greater than zero.'); + } + + if ($this->connectionLimit < 1) { + throw new InvalidArgumentException('Connection limit must be greater than zero.'); + } + } + + public static function new( + string $host = '127.0.0.1', + int $port = 8080, + int $maxPayloadBytes = 65536, + int $tickMicroseconds = 10000, + int $connectionLimit = 100, + bool $enableDebugLogs = false, + ): self { + return new self( + host: $host, + port: $port, + maxPayloadBytes: $maxPayloadBytes, + tickMicroseconds: $tickMicroseconds, + connectionLimit: $connectionLimit, + enableDebugLogs: $enableDebugLogs, + ); + } +} diff --git a/src/WebSocket.php b/src/WebSocket.php new file mode 100644 index 0000000..70570c9 --- /dev/null +++ b/src/WebSocket.php @@ -0,0 +1,13 @@ +host); + self::assertSame(8080, $config->port); + self::assertSame(65536, $config->maxPayloadBytes); + self::assertSame(10000, $config->tickMicroseconds); + self::assertSame(100, $config->connectionLimit); + self::assertFalse($config->enableDebugLogs); + } + + public function testChatConfigDefaultsAreAvailable(): void + { + $config = ChatConfig::new(); + + self::assertSame(40, $config->maxDisplayNameLength); + self::assertSame(80, $config->maxRoomNameLength); + self::assertSame(20, $config->maxPrivateGroupMembers); + self::assertTrue($config->allowGuestSessions); + self::assertSame(50, $config->historyLimit); + } +}