From 0aea371566eb3753a5648fc49001fa4de4ad668c Mon Sep 17 00:00:00 2001 From: Jaapio Date: Wed, 20 Aug 2025 23:46:10 +0200 Subject: [PATCH 1/4] Initial setup for file serve With this change we provide an internal webserver in the application. And introduce a watch on changes. This introduces a new way of working with guides, allowing users to edit documentation without caring about rerendering. Optimizations are possible like parsing and rendering a single file to make rerendering faster. --- app/Dockerfile | 10 + composer.lock | 1171 +++++++++++++---- packages/filesystem/src/FileSystem.php | 2 + packages/filesystem/src/FlySystemAdapter.php | 5 + .../src/FlysystemV1/FlysystemV1.php | 5 + .../src/FlysystemV3/FlysystemV3.php | 5 + packages/guides-cli/composer.json | 5 +- .../guides-cli/resources/config/services.php | 14 +- packages/guides-cli/src/Command/Run.php | 212 +-- packages/guides-cli/src/Command/Serve.php | 165 +++ .../src/Command/SettingsBuilder.php | 180 +++ .../guides-cli/src/Internal/RunCommand.php | 20 + .../src/Internal/RunCommandHandler.php | 113 ++ .../src/Watcher/FileModifiedEvent.php | 12 + .../guides-cli/src/Watcher/INotifyWatcher.php | 71 + 15 files changed, 1512 insertions(+), 478 deletions(-) create mode 100644 app/Dockerfile create mode 100644 packages/guides-cli/src/Command/Serve.php create mode 100644 packages/guides-cli/src/Command/SettingsBuilder.php create mode 100644 packages/guides-cli/src/Internal/RunCommand.php create mode 100644 packages/guides-cli/src/Internal/RunCommandHandler.php create mode 100644 packages/guides-cli/src/Watcher/FileModifiedEvent.php create mode 100644 packages/guides-cli/src/Watcher/INotifyWatcher.php diff --git a/app/Dockerfile b/app/Dockerfile new file mode 100644 index 000000000..6631f0702 --- /dev/null +++ b/app/Dockerfile @@ -0,0 +1,10 @@ +FROM php:8.4-cli + +COPY --from=ghcr.io/php/pie:bin /pie /usr/bin/pie + +RUN apt update && apt install -y git + +RUN docker-php-ext-configure pcntl --enable-pcntl \ + && docker-php-ext-install pcntl \ + && pie install arnaud-lb/inotify + diff --git a/composer.lock b/composer.lock index c61e357a5..c598534f7 100644 --- a/composer.lock +++ b/composer.lock @@ -206,6 +206,109 @@ ], "time": "2024-02-05T11:56:58+00:00" }, + { + "name": "evenement/evenement", + "version": "v3.0.2", + "source": { + "type": "git", + "url": "https://github.com/igorw/evenement.git", + "reference": "0a16b0d71ab13284339abb99d9d2bd813640efbc" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/igorw/evenement/zipball/0a16b0d71ab13284339abb99d9d2bd813640efbc", + "reference": "0a16b0d71ab13284339abb99d9d2bd813640efbc", + "shasum": "" + }, + "require": { + "php": ">=7.0" + }, + "require-dev": { + "phpunit/phpunit": "^9 || ^6" + }, + "type": "library", + "autoload": { + "psr-4": { + "Evenement\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Igor Wiedler", + "email": "igor@wiedler.ch" + } + ], + "description": "Événement is a very simple event dispatching library for PHP", + "keywords": [ + "event-dispatcher", + "event-emitter" + ], + "support": { + "issues": "https://github.com/igorw/evenement/issues", + "source": "https://github.com/igorw/evenement/tree/v3.0.2" + }, + "time": "2023-08-08T05:53:35+00:00" + }, + { + "name": "fig/http-message-util", + "version": "1.1.5", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-message-util.git", + "reference": "9d94dc0154230ac39e5bf89398b324a86f63f765" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-message-util/zipball/9d94dc0154230ac39e5bf89398b324a86f63f765", + "reference": "9d94dc0154230ac39e5bf89398b324a86f63f765", + "shasum": "" + }, + "require": { + "php": "^5.3 || ^7.0 || ^8.0" + }, + "suggest": { + "psr/http-message": "The package containing the PSR-7 interfaces" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.1.x-dev" + } + }, + "autoload": { + "psr-4": { + "Fig\\Http\\Message\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Utility classes and constants for use with PSR-7 (psr/http-message)", + "keywords": [ + "http", + "http-message", + "psr", + "psr-7", + "request", + "response" + ], + "support": { + "issues": "https://github.com/php-fig/http-message-util/issues", + "source": "https://github.com/php-fig/http-message-util/tree/1.1.5" + }, + "time": "2020-11-24T22:02:12+00:00" + }, { "name": "jawira/plantuml-encoding", "version": "v1.1.0", @@ -449,40 +552,42 @@ }, { "name": "league/csv", - "version": "9.15.0", + "version": "9.24.1", "source": { "type": "git", "url": "https://github.com/thephpleague/csv.git", - "reference": "fa7e2441c0bc9b2360f4314fd6c954f7ff40d435" + "reference": "e0221a3f16aa2a823047d59fab5809d552e29bc8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/thephpleague/csv/zipball/fa7e2441c0bc9b2360f4314fd6c954f7ff40d435", - "reference": "fa7e2441c0bc9b2360f4314fd6c954f7ff40d435", + "url": "https://api.github.com/repos/thephpleague/csv/zipball/e0221a3f16aa2a823047d59fab5809d552e29bc8", + "reference": "e0221a3f16aa2a823047d59fab5809d552e29bc8", "shasum": "" }, "require": { "ext-filter": "*", - "ext-json": "*", - "ext-mbstring": "*", "php": "^8.1.2" }, "require-dev": { - "doctrine/collections": "^2.1.4", "ext-dom": "*", "ext-xdebug": "*", - "friendsofphp/php-cs-fixer": "^v3.22.0", - "phpbench/phpbench": "^1.2.15", - "phpstan/phpstan": "^1.10.57", - "phpstan/phpstan-deprecation-rules": "^1.1.4", - "phpstan/phpstan-phpunit": "^1.3.15", - "phpstan/phpstan-strict-rules": "^1.5.2", - "phpunit/phpunit": "^10.5.9", - "symfony/var-dumper": "^6.4.2" + "friendsofphp/php-cs-fixer": "^3.75.0", + "phpbench/phpbench": "^1.4.1", + "phpstan/phpstan": "^1.12.27", + "phpstan/phpstan-deprecation-rules": "^1.2.1", + "phpstan/phpstan-phpunit": "^1.4.2", + "phpstan/phpstan-strict-rules": "^1.6.2", + "phpunit/phpunit": "^10.5.16 || ^11.5.22", + "symfony/var-dumper": "^6.4.8 || ^7.3.0" }, "suggest": { "ext-dom": "Required to use the XMLConverter and the HTMLConverter classes", - "ext-iconv": "Needed to ease transcoding CSV using iconv stream filters" + "ext-iconv": "Needed to ease transcoding CSV using iconv stream filters", + "ext-mbstring": "Needed to ease transcoding CSV using mb stream filters", + "ext-mysqli": "Requiered to use the package with the MySQLi extension", + "ext-pdo": "Required to use the package with the PDO extension", + "ext-pgsql": "Requiered to use the package with the PgSQL extension", + "ext-sqlite3": "Required to use the package with the SQLite3 extension" }, "type": "library", "extra": { @@ -534,20 +639,20 @@ "type": "github" } ], - "time": "2024-02-20T20:00:00+00:00" + "time": "2025-06-25T14:53:51+00:00" }, { "name": "league/flysystem", - "version": "3.29.1", + "version": "3.30.0", "source": { "type": "git", "url": "https://github.com/thephpleague/flysystem.git", - "reference": "edc1bb7c86fab0776c3287dbd19b5fa278347319" + "reference": "2203e3151755d874bb2943649dae1eb8533ac93e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/thephpleague/flysystem/zipball/edc1bb7c86fab0776c3287dbd19b5fa278347319", - "reference": "edc1bb7c86fab0776c3287dbd19b5fa278347319", + "url": "https://api.github.com/repos/thephpleague/flysystem/zipball/2203e3151755d874bb2943649dae1eb8533ac93e", + "reference": "2203e3151755d874bb2943649dae1eb8533ac93e", "shasum": "" }, "require": { @@ -571,13 +676,13 @@ "composer/semver": "^3.0", "ext-fileinfo": "*", "ext-ftp": "*", - "ext-mongodb": "^1.3", + "ext-mongodb": "^1.3|^2", "ext-zip": "*", "friendsofphp/php-cs-fixer": "^3.5", "google/cloud-storage": "^1.23", "guzzlehttp/psr7": "^2.6", "microsoft/azure-storage-blob": "^1.1", - "mongodb/mongodb": "^1.2", + "mongodb/mongodb": "^1.2|^2", "phpseclib/phpseclib": "^3.0.36", "phpstan/phpstan": "^1.10", "phpunit/phpunit": "^9.5.11|^10.0", @@ -615,22 +720,22 @@ ], "support": { "issues": "https://github.com/thephpleague/flysystem/issues", - "source": "https://github.com/thephpleague/flysystem/tree/3.29.1" + "source": "https://github.com/thephpleague/flysystem/tree/3.30.0" }, - "time": "2024-10-08T08:58:34+00:00" + "time": "2025-06-25T13:29:59+00:00" }, { "name": "league/flysystem-local", - "version": "3.29.0", + "version": "3.30.0", "source": { "type": "git", "url": "https://github.com/thephpleague/flysystem-local.git", - "reference": "e0e8d52ce4b2ed154148453d321e97c8e931bd27" + "reference": "6691915f77c7fb69adfb87dcd550052dc184ee10" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/thephpleague/flysystem-local/zipball/e0e8d52ce4b2ed154148453d321e97c8e931bd27", - "reference": "e0e8d52ce4b2ed154148453d321e97c8e931bd27", + "url": "https://api.github.com/repos/thephpleague/flysystem-local/zipball/6691915f77c7fb69adfb87dcd550052dc184ee10", + "reference": "6691915f77c7fb69adfb87dcd550052dc184ee10", "shasum": "" }, "require": { @@ -664,9 +769,9 @@ "local" ], "support": { - "source": "https://github.com/thephpleague/flysystem-local/tree/3.29.0" + "source": "https://github.com/thephpleague/flysystem-local/tree/3.30.0" }, - "time": "2024-08-09T21:24:39+00:00" + "time": "2025-05-21T10:34:19+00:00" }, { "name": "league/mime-type-detection", @@ -955,16 +1060,16 @@ }, { "name": "masterminds/html5", - "version": "2.9.0", + "version": "2.10.0", "source": { "type": "git", "url": "https://github.com/Masterminds/html5-php.git", - "reference": "f5ac2c0b0a2eefca70b2ce32a5809992227e75a6" + "reference": "fcf91eb64359852f00d921887b219479b4f21251" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Masterminds/html5-php/zipball/f5ac2c0b0a2eefca70b2ce32a5809992227e75a6", - "reference": "f5ac2c0b0a2eefca70b2ce32a5809992227e75a6", + "url": "https://api.github.com/repos/Masterminds/html5-php/zipball/fcf91eb64359852f00d921887b219479b4f21251", + "reference": "fcf91eb64359852f00d921887b219479b4f21251", "shasum": "" }, "require": { @@ -1016,22 +1121,22 @@ ], "support": { "issues": "https://github.com/Masterminds/html5-php/issues", - "source": "https://github.com/Masterminds/html5-php/tree/2.9.0" + "source": "https://github.com/Masterminds/html5-php/tree/2.10.0" }, - "time": "2024-03-31T07:05:07+00:00" + "time": "2025-07-25T09:04:22+00:00" }, { "name": "monolog/monolog", - "version": "3.6.0", + "version": "3.9.0", "source": { "type": "git", "url": "https://github.com/Seldaek/monolog.git", - "reference": "4b18b21a5527a3d5ffdac2fd35d3ab25a9597654" + "reference": "10d85740180ecba7896c87e06a166e0c95a0e3b6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Seldaek/monolog/zipball/4b18b21a5527a3d5ffdac2fd35d3ab25a9597654", - "reference": "4b18b21a5527a3d5ffdac2fd35d3ab25a9597654", + "url": "https://api.github.com/repos/Seldaek/monolog/zipball/10d85740180ecba7896c87e06a166e0c95a0e3b6", + "reference": "10d85740180ecba7896c87e06a166e0c95a0e3b6", "shasum": "" }, "require": { @@ -1051,12 +1156,14 @@ "guzzlehttp/psr7": "^2.2", "mongodb/mongodb": "^1.8", "php-amqplib/php-amqplib": "~2.4 || ^3", - "phpstan/phpstan": "^1.9", - "phpstan/phpstan-deprecation-rules": "^1.0", - "phpstan/phpstan-strict-rules": "^1.4", - "phpunit/phpunit": "^10.5.17", + "php-console/php-console": "^3.1.8", + "phpstan/phpstan": "^2", + "phpstan/phpstan-deprecation-rules": "^2", + "phpstan/phpstan-strict-rules": "^2", + "phpunit/phpunit": "^10.5.17 || ^11.0.7", "predis/predis": "^1.1 || ^2", - "ruflin/elastica": "^7", + "rollbar/rollbar": "^4.0", + "ruflin/elastica": "^7 || ^8", "symfony/mailer": "^5.4 || ^6", "symfony/mime": "^5.4 || ^6" }, @@ -1107,7 +1214,7 @@ ], "support": { "issues": "https://github.com/Seldaek/monolog/issues", - "source": "https://github.com/Seldaek/monolog/tree/3.6.0" + "source": "https://github.com/Seldaek/monolog/tree/3.9.0" }, "funding": [ { @@ -1119,7 +1226,7 @@ "type": "tidelift" } ], - "time": "2024-04-12T21:02:21+00:00" + "time": "2025-03-24T10:02:05+00:00" }, { "name": "nette/schema", @@ -1430,13 +1537,16 @@ "dist": { "type": "path", "url": "./packages/guides-cli", - "reference": "01b63176c171294709d3f7b71eb35d3a02d40d21" + "reference": "92668cc362e81169056a7897e8444e1ef62ea030" }, "require": { + "league/mime-type-detection": "^1.16", "monolog/monolog": "^3.0", "php": "^8.1", "phpdocumentor/guides": "^1.0 || ^2.0", "phpdocumentor/guides-restructured-text": "^1.0 || ^2.0", + "react/http": "^v1.11", + "react/socket": "^v1.16", "symfony/config": "^5.4 || ^6.3 || ^7.0", "symfony/console": "^5.4 || ^6.3 || ^7.0", "symfony/dependency-injection": "^5.4 || ^6.3 || ^7.0", @@ -1926,16 +2036,16 @@ }, { "name": "psr/http-message", - "version": "2.0", + "version": "1.1", "source": { "type": "git", "url": "https://github.com/php-fig/http-message.git", - "reference": "402d35bcb92c70c026d1a6a9883f06b2ead23d71" + "reference": "cb6ce4845ce34a8ad9e68117c10ee90a29919eba" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/php-fig/http-message/zipball/402d35bcb92c70c026d1a6a9883f06b2ead23d71", - "reference": "402d35bcb92c70c026d1a6a9883f06b2ead23d71", + "url": "https://api.github.com/repos/php-fig/http-message/zipball/cb6ce4845ce34a8ad9e68117c10ee90a29919eba", + "reference": "cb6ce4845ce34a8ad9e68117c10ee90a29919eba", "shasum": "" }, "require": { @@ -1944,7 +2054,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "2.0.x-dev" + "dev-master": "1.1.x-dev" } }, "autoload": { @@ -1959,7 +2069,7 @@ "authors": [ { "name": "PHP-FIG", - "homepage": "https://www.php-fig.org/" + "homepage": "http://www.php-fig.org/" } ], "description": "Common interface for HTTP messages", @@ -1973,9 +2083,9 @@ "response" ], "support": { - "source": "https://github.com/php-fig/http-message/tree/2.0" + "source": "https://github.com/php-fig/http-message/tree/1.1" }, - "time": "2023-04-04T09:54:51+00:00" + "time": "2023-04-04T09:50:52+00:00" }, { "name": "psr/log", @@ -2027,6 +2137,548 @@ }, "time": "2024-09-11T13:17:53+00:00" }, + { + "name": "react/cache", + "version": "v1.2.0", + "source": { + "type": "git", + "url": "https://github.com/reactphp/cache.git", + "reference": "d47c472b64aa5608225f47965a484b75c7817d5b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/reactphp/cache/zipball/d47c472b64aa5608225f47965a484b75c7817d5b", + "reference": "d47c472b64aa5608225f47965a484b75c7817d5b", + "shasum": "" + }, + "require": { + "php": ">=5.3.0", + "react/promise": "^3.0 || ^2.0 || ^1.1" + }, + "require-dev": { + "phpunit/phpunit": "^9.5 || ^5.7 || ^4.8.35" + }, + "type": "library", + "autoload": { + "psr-4": { + "React\\Cache\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Christian Lück", + "email": "christian@clue.engineering", + "homepage": "https://clue.engineering/" + }, + { + "name": "Cees-Jan Kiewiet", + "email": "reactphp@ceesjankiewiet.nl", + "homepage": "https://wyrihaximus.net/" + }, + { + "name": "Jan Sorgalla", + "email": "jsorgalla@gmail.com", + "homepage": "https://sorgalla.com/" + }, + { + "name": "Chris Boden", + "email": "cboden@gmail.com", + "homepage": "https://cboden.dev/" + } + ], + "description": "Async, Promise-based cache interface for ReactPHP", + "keywords": [ + "cache", + "caching", + "promise", + "reactphp" + ], + "support": { + "issues": "https://github.com/reactphp/cache/issues", + "source": "https://github.com/reactphp/cache/tree/v1.2.0" + }, + "funding": [ + { + "url": "https://opencollective.com/reactphp", + "type": "open_collective" + } + ], + "time": "2022-11-30T15:59:55+00:00" + }, + { + "name": "react/dns", + "version": "v1.13.0", + "source": { + "type": "git", + "url": "https://github.com/reactphp/dns.git", + "reference": "eb8ae001b5a455665c89c1df97f6fb682f8fb0f5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/reactphp/dns/zipball/eb8ae001b5a455665c89c1df97f6fb682f8fb0f5", + "reference": "eb8ae001b5a455665c89c1df97f6fb682f8fb0f5", + "shasum": "" + }, + "require": { + "php": ">=5.3.0", + "react/cache": "^1.0 || ^0.6 || ^0.5", + "react/event-loop": "^1.2", + "react/promise": "^3.2 || ^2.7 || ^1.2.1" + }, + "require-dev": { + "phpunit/phpunit": "^9.6 || ^5.7 || ^4.8.36", + "react/async": "^4.3 || ^3 || ^2", + "react/promise-timer": "^1.11" + }, + "type": "library", + "autoload": { + "psr-4": { + "React\\Dns\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Christian Lück", + "email": "christian@clue.engineering", + "homepage": "https://clue.engineering/" + }, + { + "name": "Cees-Jan Kiewiet", + "email": "reactphp@ceesjankiewiet.nl", + "homepage": "https://wyrihaximus.net/" + }, + { + "name": "Jan Sorgalla", + "email": "jsorgalla@gmail.com", + "homepage": "https://sorgalla.com/" + }, + { + "name": "Chris Boden", + "email": "cboden@gmail.com", + "homepage": "https://cboden.dev/" + } + ], + "description": "Async DNS resolver for ReactPHP", + "keywords": [ + "async", + "dns", + "dns-resolver", + "reactphp" + ], + "support": { + "issues": "https://github.com/reactphp/dns/issues", + "source": "https://github.com/reactphp/dns/tree/v1.13.0" + }, + "funding": [ + { + "url": "https://opencollective.com/reactphp", + "type": "open_collective" + } + ], + "time": "2024-06-13T14:18:03+00:00" + }, + { + "name": "react/event-loop", + "version": "v1.5.0", + "source": { + "type": "git", + "url": "https://github.com/reactphp/event-loop.git", + "reference": "bbe0bd8c51ffc05ee43f1729087ed3bdf7d53354" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/reactphp/event-loop/zipball/bbe0bd8c51ffc05ee43f1729087ed3bdf7d53354", + "reference": "bbe0bd8c51ffc05ee43f1729087ed3bdf7d53354", + "shasum": "" + }, + "require": { + "php": ">=5.3.0" + }, + "require-dev": { + "phpunit/phpunit": "^9.6 || ^5.7 || ^4.8.36" + }, + "suggest": { + "ext-pcntl": "For signal handling support when using the StreamSelectLoop" + }, + "type": "library", + "autoload": { + "psr-4": { + "React\\EventLoop\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Christian Lück", + "email": "christian@clue.engineering", + "homepage": "https://clue.engineering/" + }, + { + "name": "Cees-Jan Kiewiet", + "email": "reactphp@ceesjankiewiet.nl", + "homepage": "https://wyrihaximus.net/" + }, + { + "name": "Jan Sorgalla", + "email": "jsorgalla@gmail.com", + "homepage": "https://sorgalla.com/" + }, + { + "name": "Chris Boden", + "email": "cboden@gmail.com", + "homepage": "https://cboden.dev/" + } + ], + "description": "ReactPHP's core reactor event loop that libraries can use for evented I/O.", + "keywords": [ + "asynchronous", + "event-loop" + ], + "support": { + "issues": "https://github.com/reactphp/event-loop/issues", + "source": "https://github.com/reactphp/event-loop/tree/v1.5.0" + }, + "funding": [ + { + "url": "https://opencollective.com/reactphp", + "type": "open_collective" + } + ], + "time": "2023-11-13T13:48:05+00:00" + }, + { + "name": "react/http", + "version": "v1.11.0", + "source": { + "type": "git", + "url": "https://github.com/reactphp/http.git", + "reference": "8db02de41dcca82037367f67a2d4be365b1c4db9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/reactphp/http/zipball/8db02de41dcca82037367f67a2d4be365b1c4db9", + "reference": "8db02de41dcca82037367f67a2d4be365b1c4db9", + "shasum": "" + }, + "require": { + "evenement/evenement": "^3.0 || ^2.0 || ^1.0", + "fig/http-message-util": "^1.1", + "php": ">=5.3.0", + "psr/http-message": "^1.0", + "react/event-loop": "^1.2", + "react/promise": "^3.2 || ^2.3 || ^1.2.1", + "react/socket": "^1.16", + "react/stream": "^1.4" + }, + "require-dev": { + "clue/http-proxy-react": "^1.8", + "clue/reactphp-ssh-proxy": "^1.4", + "clue/socks-react": "^1.4", + "phpunit/phpunit": "^9.6 || ^5.7 || ^4.8.36", + "react/async": "^4.2 || ^3 || ^2", + "react/promise-stream": "^1.4", + "react/promise-timer": "^1.11" + }, + "type": "library", + "autoload": { + "psr-4": { + "React\\Http\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Christian Lück", + "email": "christian@clue.engineering", + "homepage": "https://clue.engineering/" + }, + { + "name": "Cees-Jan Kiewiet", + "email": "reactphp@ceesjankiewiet.nl", + "homepage": "https://wyrihaximus.net/" + }, + { + "name": "Jan Sorgalla", + "email": "jsorgalla@gmail.com", + "homepage": "https://sorgalla.com/" + }, + { + "name": "Chris Boden", + "email": "cboden@gmail.com", + "homepage": "https://cboden.dev/" + } + ], + "description": "Event-driven, streaming HTTP client and server implementation for ReactPHP", + "keywords": [ + "async", + "client", + "event-driven", + "http", + "http client", + "http server", + "https", + "psr-7", + "reactphp", + "server", + "streaming" + ], + "support": { + "issues": "https://github.com/reactphp/http/issues", + "source": "https://github.com/reactphp/http/tree/v1.11.0" + }, + "funding": [ + { + "url": "https://opencollective.com/reactphp", + "type": "open_collective" + } + ], + "time": "2024-11-20T15:24:08+00:00" + }, + { + "name": "react/promise", + "version": "v3.2.0", + "source": { + "type": "git", + "url": "https://github.com/reactphp/promise.git", + "reference": "8a164643313c71354582dc850b42b33fa12a4b63" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/reactphp/promise/zipball/8a164643313c71354582dc850b42b33fa12a4b63", + "reference": "8a164643313c71354582dc850b42b33fa12a4b63", + "shasum": "" + }, + "require": { + "php": ">=7.1.0" + }, + "require-dev": { + "phpstan/phpstan": "1.10.39 || 1.4.10", + "phpunit/phpunit": "^9.6 || ^7.5" + }, + "type": "library", + "autoload": { + "files": [ + "src/functions_include.php" + ], + "psr-4": { + "React\\Promise\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jan Sorgalla", + "email": "jsorgalla@gmail.com", + "homepage": "https://sorgalla.com/" + }, + { + "name": "Christian Lück", + "email": "christian@clue.engineering", + "homepage": "https://clue.engineering/" + }, + { + "name": "Cees-Jan Kiewiet", + "email": "reactphp@ceesjankiewiet.nl", + "homepage": "https://wyrihaximus.net/" + }, + { + "name": "Chris Boden", + "email": "cboden@gmail.com", + "homepage": "https://cboden.dev/" + } + ], + "description": "A lightweight implementation of CommonJS Promises/A for PHP", + "keywords": [ + "promise", + "promises" + ], + "support": { + "issues": "https://github.com/reactphp/promise/issues", + "source": "https://github.com/reactphp/promise/tree/v3.2.0" + }, + "funding": [ + { + "url": "https://opencollective.com/reactphp", + "type": "open_collective" + } + ], + "time": "2024-05-24T10:39:05+00:00" + }, + { + "name": "react/socket", + "version": "v1.16.0", + "source": { + "type": "git", + "url": "https://github.com/reactphp/socket.git", + "reference": "23e4ff33ea3e160d2d1f59a0e6050e4b0fb0eac1" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/reactphp/socket/zipball/23e4ff33ea3e160d2d1f59a0e6050e4b0fb0eac1", + "reference": "23e4ff33ea3e160d2d1f59a0e6050e4b0fb0eac1", + "shasum": "" + }, + "require": { + "evenement/evenement": "^3.0 || ^2.0 || ^1.0", + "php": ">=5.3.0", + "react/dns": "^1.13", + "react/event-loop": "^1.2", + "react/promise": "^3.2 || ^2.6 || ^1.2.1", + "react/stream": "^1.4" + }, + "require-dev": { + "phpunit/phpunit": "^9.6 || ^5.7 || ^4.8.36", + "react/async": "^4.3 || ^3.3 || ^2", + "react/promise-stream": "^1.4", + "react/promise-timer": "^1.11" + }, + "type": "library", + "autoload": { + "psr-4": { + "React\\Socket\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Christian Lück", + "email": "christian@clue.engineering", + "homepage": "https://clue.engineering/" + }, + { + "name": "Cees-Jan Kiewiet", + "email": "reactphp@ceesjankiewiet.nl", + "homepage": "https://wyrihaximus.net/" + }, + { + "name": "Jan Sorgalla", + "email": "jsorgalla@gmail.com", + "homepage": "https://sorgalla.com/" + }, + { + "name": "Chris Boden", + "email": "cboden@gmail.com", + "homepage": "https://cboden.dev/" + } + ], + "description": "Async, streaming plaintext TCP/IP and secure TLS socket server and client connections for ReactPHP", + "keywords": [ + "Connection", + "Socket", + "async", + "reactphp", + "stream" + ], + "support": { + "issues": "https://github.com/reactphp/socket/issues", + "source": "https://github.com/reactphp/socket/tree/v1.16.0" + }, + "funding": [ + { + "url": "https://opencollective.com/reactphp", + "type": "open_collective" + } + ], + "time": "2024-07-26T10:38:09+00:00" + }, + { + "name": "react/stream", + "version": "v1.4.0", + "source": { + "type": "git", + "url": "https://github.com/reactphp/stream.git", + "reference": "1e5b0acb8fe55143b5b426817155190eb6f5b18d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/reactphp/stream/zipball/1e5b0acb8fe55143b5b426817155190eb6f5b18d", + "reference": "1e5b0acb8fe55143b5b426817155190eb6f5b18d", + "shasum": "" + }, + "require": { + "evenement/evenement": "^3.0 || ^2.0 || ^1.0", + "php": ">=5.3.8", + "react/event-loop": "^1.2" + }, + "require-dev": { + "clue/stream-filter": "~1.2", + "phpunit/phpunit": "^9.6 || ^5.7 || ^4.8.36" + }, + "type": "library", + "autoload": { + "psr-4": { + "React\\Stream\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Christian Lück", + "email": "christian@clue.engineering", + "homepage": "https://clue.engineering/" + }, + { + "name": "Cees-Jan Kiewiet", + "email": "reactphp@ceesjankiewiet.nl", + "homepage": "https://wyrihaximus.net/" + }, + { + "name": "Jan Sorgalla", + "email": "jsorgalla@gmail.com", + "homepage": "https://sorgalla.com/" + }, + { + "name": "Chris Boden", + "email": "cboden@gmail.com", + "homepage": "https://cboden.dev/" + } + ], + "description": "Event-driven readable and writable streams for non-blocking I/O in ReactPHP", + "keywords": [ + "event-driven", + "io", + "non-blocking", + "pipe", + "reactphp", + "readable", + "stream", + "writable" + ], + "support": { + "issues": "https://github.com/reactphp/stream/issues", + "source": "https://github.com/reactphp/stream/tree/v1.4.0" + }, + "funding": [ + { + "url": "https://opencollective.com/reactphp", + "type": "open_collective" + } + ], + "time": "2024-06-11T12:45:25+00:00" + }, { "name": "scrivo/highlight.php", "version": "v9.18.1.10", @@ -2107,16 +2759,16 @@ }, { "name": "symfony/clock", - "version": "v6.4.8", + "version": "v6.4.24", "source": { "type": "git", "url": "https://github.com/symfony/clock.git", - "reference": "7a4840efd17135cbd547e41ec49fb910ed4f8b98" + "reference": "5e15a9c9aeeb44a99f7cf24aa75aa9607795f6f8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/clock/zipball/7a4840efd17135cbd547e41ec49fb910ed4f8b98", - "reference": "7a4840efd17135cbd547e41ec49fb910ed4f8b98", + "url": "https://api.github.com/repos/symfony/clock/zipball/5e15a9c9aeeb44a99f7cf24aa75aa9607795f6f8", + "reference": "5e15a9c9aeeb44a99f7cf24aa75aa9607795f6f8", "shasum": "" }, "require": { @@ -2161,7 +2813,7 @@ "time" ], "support": { - "source": "https://github.com/symfony/clock/tree/v6.4.8" + "source": "https://github.com/symfony/clock/tree/v6.4.24" }, "funding": [ { @@ -2172,25 +2824,29 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2024-05-31T14:51:39+00:00" + "time": "2025-07-10T08:14:14+00:00" }, { "name": "symfony/config", - "version": "v6.4.7", + "version": "v6.4.24", "source": { "type": "git", "url": "https://github.com/symfony/config.git", - "reference": "51da0e4494d81bd7b5b5bd80319c55d8e0d7f4ff" + "reference": "80e2cf005cf17138c97193be0434cdcfd1b2212e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/config/zipball/51da0e4494d81bd7b5b5bd80319c55d8e0d7f4ff", - "reference": "51da0e4494d81bd7b5b5bd80319c55d8e0d7f4ff", + "url": "https://api.github.com/repos/symfony/config/zipball/80e2cf005cf17138c97193be0434cdcfd1b2212e", + "reference": "80e2cf005cf17138c97193be0434cdcfd1b2212e", "shasum": "" }, "require": { @@ -2236,7 +2892,7 @@ "description": "Helps you find, load, combine, autofill and validate configuration values of any kind", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/config/tree/v6.4.7" + "source": "https://github.com/symfony/config/tree/v6.4.24" }, "funding": [ { @@ -2247,25 +2903,29 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2024-04-18T09:22:46+00:00" + "time": "2025-07-26T13:50:30+00:00" }, { "name": "symfony/console", - "version": "v6.4.17", + "version": "v6.4.24", "source": { "type": "git", "url": "https://github.com/symfony/console.git", - "reference": "799445db3f15768ecc382ac5699e6da0520a0a04" + "reference": "59266a5bf6a596e3e0844fd95e6ad7ea3c1d3350" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/799445db3f15768ecc382ac5699e6da0520a0a04", - "reference": "799445db3f15768ecc382ac5699e6da0520a0a04", + "url": "https://api.github.com/repos/symfony/console/zipball/59266a5bf6a596e3e0844fd95e6ad7ea3c1d3350", + "reference": "59266a5bf6a596e3e0844fd95e6ad7ea3c1d3350", "shasum": "" }, "require": { @@ -2330,7 +2990,7 @@ "terminal" ], "support": { - "source": "https://github.com/symfony/console/tree/v6.4.17" + "source": "https://github.com/symfony/console/tree/v6.4.24" }, "funding": [ { @@ -2341,25 +3001,29 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2024-12-07T12:07:30+00:00" + "time": "2025-07-30T10:38:54+00:00" }, { "name": "symfony/dependency-injection", - "version": "v6.4.7", + "version": "v6.4.24", "source": { "type": "git", "url": "https://github.com/symfony/dependency-injection.git", - "reference": "d8c5f9781b71c2a868ae9d0e5c9b283684740b6d" + "reference": "929ab73b93247a15166ee79e807ccee4f930322d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/dependency-injection/zipball/d8c5f9781b71c2a868ae9d0e5c9b283684740b6d", - "reference": "d8c5f9781b71c2a868ae9d0e5c9b283684740b6d", + "url": "https://api.github.com/repos/symfony/dependency-injection/zipball/929ab73b93247a15166ee79e807ccee4f930322d", + "reference": "929ab73b93247a15166ee79e807ccee4f930322d", "shasum": "" }, "require": { @@ -2367,7 +3031,7 @@ "psr/container": "^1.1|^2.0", "symfony/deprecation-contracts": "^2.5|^3", "symfony/service-contracts": "^2.5|^3.0", - "symfony/var-exporter": "^6.2.10|^7.0" + "symfony/var-exporter": "^6.4.20|^7.2.5" }, "conflict": { "ext-psr": "<1.1|>=2", @@ -2411,7 +3075,7 @@ "description": "Allows you to standardize and centralize the way objects are constructed in your application", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/dependency-injection/tree/v6.4.7" + "source": "https://github.com/symfony/dependency-injection/tree/v6.4.24" }, "funding": [ { @@ -2422,25 +3086,29 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2024-04-18T09:22:46+00:00" + "time": "2025-07-30T17:30:48+00:00" }, { "name": "symfony/deprecation-contracts", - "version": "v3.5.1", + "version": "v3.6.0", "source": { "type": "git", "url": "https://github.com/symfony/deprecation-contracts.git", - "reference": "74c71c939a79f7d5bf3c1ce9f5ea37ba0114c6f6" + "reference": "63afe740e99a13ba87ec199bb07bbdee937a5b62" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/74c71c939a79f7d5bf3c1ce9f5ea37ba0114c6f6", - "reference": "74c71c939a79f7d5bf3c1ce9f5ea37ba0114c6f6", + "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/63afe740e99a13ba87ec199bb07bbdee937a5b62", + "reference": "63afe740e99a13ba87ec199bb07bbdee937a5b62", "shasum": "" }, "require": { @@ -2453,7 +3121,7 @@ "name": "symfony/contracts" }, "branch-alias": { - "dev-main": "3.5-dev" + "dev-main": "3.6-dev" } }, "autoload": { @@ -2478,7 +3146,7 @@ "description": "A generic function and convention to trigger deprecation notices", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/deprecation-contracts/tree/v3.5.1" + "source": "https://github.com/symfony/deprecation-contracts/tree/v3.6.0" }, "funding": [ { @@ -2494,20 +3162,20 @@ "type": "tidelift" } ], - "time": "2024-09-25T14:20:29+00:00" + "time": "2024-09-25T14:21:43+00:00" }, { "name": "symfony/event-dispatcher", - "version": "v6.4.7", + "version": "v6.4.24", "source": { "type": "git", "url": "https://github.com/symfony/event-dispatcher.git", - "reference": "d84384f3f67de3cb650db64d685d70395dacfc3f" + "reference": "307a09d8d7228d14a05e5e05b95fffdacab032b2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/d84384f3f67de3cb650db64d685d70395dacfc3f", - "reference": "d84384f3f67de3cb650db64d685d70395dacfc3f", + "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/307a09d8d7228d14a05e5e05b95fffdacab032b2", + "reference": "307a09d8d7228d14a05e5e05b95fffdacab032b2", "shasum": "" }, "require": { @@ -2558,7 +3226,7 @@ "description": "Provides tools that allow your application components to communicate with each other by dispatching events and listening to them", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/event-dispatcher/tree/v6.4.7" + "source": "https://github.com/symfony/event-dispatcher/tree/v6.4.24" }, "funding": [ { @@ -2569,25 +3237,29 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2024-04-18T09:22:46+00:00" + "time": "2025-07-10T08:14:14+00:00" }, { "name": "symfony/event-dispatcher-contracts", - "version": "v3.5.0", + "version": "v3.6.0", "source": { "type": "git", "url": "https://github.com/symfony/event-dispatcher-contracts.git", - "reference": "8f93aec25d41b72493c6ddff14e916177c9efc50" + "reference": "59eb412e93815df44f05f342958efa9f46b1e586" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/event-dispatcher-contracts/zipball/8f93aec25d41b72493c6ddff14e916177c9efc50", - "reference": "8f93aec25d41b72493c6ddff14e916177c9efc50", + "url": "https://api.github.com/repos/symfony/event-dispatcher-contracts/zipball/59eb412e93815df44f05f342958efa9f46b1e586", + "reference": "59eb412e93815df44f05f342958efa9f46b1e586", "shasum": "" }, "require": { @@ -2596,12 +3268,12 @@ }, "type": "library", "extra": { - "branch-alias": { - "dev-main": "3.5-dev" - }, "thanks": { - "name": "symfony/contracts", - "url": "https://github.com/symfony/contracts" + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, + "branch-alias": { + "dev-main": "3.6-dev" } }, "autoload": { @@ -2634,7 +3306,7 @@ "standards" ], "support": { - "source": "https://github.com/symfony/event-dispatcher-contracts/tree/v3.5.0" + "source": "https://github.com/symfony/event-dispatcher-contracts/tree/v3.6.0" }, "funding": [ { @@ -2650,20 +3322,20 @@ "type": "tidelift" } ], - "time": "2024-04-18T09:32:20+00:00" + "time": "2024-09-25T14:21:43+00:00" }, { "name": "symfony/filesystem", - "version": "v6.4.13", + "version": "v6.4.24", "source": { "type": "git", "url": "https://github.com/symfony/filesystem.git", - "reference": "4856c9cf585d5a0313d8d35afd681a526f038dd3" + "reference": "75ae2edb7cdcc0c53766c30b0a2512b8df574bd8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/filesystem/zipball/4856c9cf585d5a0313d8d35afd681a526f038dd3", - "reference": "4856c9cf585d5a0313d8d35afd681a526f038dd3", + "url": "https://api.github.com/repos/symfony/filesystem/zipball/75ae2edb7cdcc0c53766c30b0a2512b8df574bd8", + "reference": "75ae2edb7cdcc0c53766c30b0a2512b8df574bd8", "shasum": "" }, "require": { @@ -2700,7 +3372,7 @@ "description": "Provides basic utilities for the filesystem", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/filesystem/tree/v6.4.13" + "source": "https://github.com/symfony/filesystem/tree/v6.4.24" }, "funding": [ { @@ -2711,25 +3383,29 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2024-10-25T15:07:50+00:00" + "time": "2025-07-10T08:14:14+00:00" }, { "name": "symfony/html-sanitizer", - "version": "v6.4.12", + "version": "v6.4.24", "source": { "type": "git", "url": "https://github.com/symfony/html-sanitizer.git", - "reference": "b58efe8ed0d8f5bf84913380a2f9da0c242f4200" + "reference": "8e9bb309986809af4cd9e049f9362d736387f083" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/html-sanitizer/zipball/b58efe8ed0d8f5bf84913380a2f9da0c242f4200", - "reference": "b58efe8ed0d8f5bf84913380a2f9da0c242f4200", + "url": "https://api.github.com/repos/symfony/html-sanitizer/zipball/8e9bb309986809af4cd9e049f9362d736387f083", + "reference": "8e9bb309986809af4cd9e049f9362d736387f083", "shasum": "" }, "require": { @@ -2769,7 +3445,7 @@ "sanitizer" ], "support": { - "source": "https://github.com/symfony/html-sanitizer/tree/v6.4.12" + "source": "https://github.com/symfony/html-sanitizer/tree/v6.4.24" }, "funding": [ { @@ -2780,32 +3456,36 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2024-09-20T08:21:33+00:00" + "time": "2025-07-10T08:14:14+00:00" }, { "name": "symfony/http-client", - "version": "v6.4.12", + "version": "v6.4.24", "source": { "type": "git", "url": "https://github.com/symfony/http-client.git", - "reference": "fbebfcce21084d3e91ea987ae5bdd8c71ff0fd56" + "reference": "6d78fe8abecd547c159b8a49f7c88610630b7da2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-client/zipball/fbebfcce21084d3e91ea987ae5bdd8c71ff0fd56", - "reference": "fbebfcce21084d3e91ea987ae5bdd8c71ff0fd56", + "url": "https://api.github.com/repos/symfony/http-client/zipball/6d78fe8abecd547c159b8a49f7c88610630b7da2", + "reference": "6d78fe8abecd547c159b8a49f7c88610630b7da2", "shasum": "" }, "require": { "php": ">=8.1", "psr/log": "^1|^2|^3", "symfony/deprecation-contracts": "^2.5|^3", - "symfony/http-client-contracts": "^3.4.1", + "symfony/http-client-contracts": "~3.4.4|^3.5.2", "symfony/service-contracts": "^2.5|^3" }, "conflict": { @@ -2862,7 +3542,7 @@ "http" ], "support": { - "source": "https://github.com/symfony/http-client/tree/v6.4.12" + "source": "https://github.com/symfony/http-client/tree/v6.4.24" }, "funding": [ { @@ -2873,25 +3553,29 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2024-09-20T08:21:33+00:00" + "time": "2025-07-14T16:38:25+00:00" }, { "name": "symfony/http-client-contracts", - "version": "v3.5.0", + "version": "v3.6.0", "source": { "type": "git", "url": "https://github.com/symfony/http-client-contracts.git", - "reference": "20414d96f391677bf80078aa55baece78b82647d" + "reference": "75d7043853a42837e68111812f4d964b01e5101c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-client-contracts/zipball/20414d96f391677bf80078aa55baece78b82647d", - "reference": "20414d96f391677bf80078aa55baece78b82647d", + "url": "https://api.github.com/repos/symfony/http-client-contracts/zipball/75d7043853a42837e68111812f4d964b01e5101c", + "reference": "75d7043853a42837e68111812f4d964b01e5101c", "shasum": "" }, "require": { @@ -2899,12 +3583,12 @@ }, "type": "library", "extra": { - "branch-alias": { - "dev-main": "3.5-dev" - }, "thanks": { - "name": "symfony/contracts", - "url": "https://github.com/symfony/contracts" + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, + "branch-alias": { + "dev-main": "3.6-dev" } }, "autoload": { @@ -2940,7 +3624,7 @@ "standards" ], "support": { - "source": "https://github.com/symfony/http-client-contracts/tree/v3.5.0" + "source": "https://github.com/symfony/http-client-contracts/tree/v3.6.0" }, "funding": [ { @@ -2956,11 +3640,11 @@ "type": "tidelift" } ], - "time": "2024-04-18T09:32:20+00:00" + "time": "2025-04-29T11:18:49+00:00" }, { "name": "symfony/polyfill-ctype", - "version": "v1.31.0", + "version": "v1.32.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-ctype.git", @@ -3019,7 +3703,7 @@ "portable" ], "support": { - "source": "https://github.com/symfony/polyfill-ctype/tree/v1.31.0" + "source": "https://github.com/symfony/polyfill-ctype/tree/v1.32.0" }, "funding": [ { @@ -3039,7 +3723,7 @@ }, { "name": "symfony/polyfill-intl-grapheme", - "version": "v1.31.0", + "version": "v1.32.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-intl-grapheme.git", @@ -3097,7 +3781,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.31.0" + "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.32.0" }, "funding": [ { @@ -3117,7 +3801,7 @@ }, { "name": "symfony/polyfill-intl-normalizer", - "version": "v1.31.0", + "version": "v1.32.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-intl-normalizer.git", @@ -3178,7 +3862,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.31.0" + "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.32.0" }, "funding": [ { @@ -3198,19 +3882,20 @@ }, { "name": "symfony/polyfill-mbstring", - "version": "v1.31.0", + "version": "v1.32.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-mbstring.git", - "reference": "85181ba99b2345b0ef10ce42ecac37612d9fd341" + "reference": "6d857f4d76bd4b343eac26d6b539585d2bc56493" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/85181ba99b2345b0ef10ce42ecac37612d9fd341", - "reference": "85181ba99b2345b0ef10ce42ecac37612d9fd341", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/6d857f4d76bd4b343eac26d6b539585d2bc56493", + "reference": "6d857f4d76bd4b343eac26d6b539585d2bc56493", "shasum": "" }, "require": { + "ext-iconv": "*", "php": ">=7.2" }, "provide": { @@ -3258,7 +3943,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.31.0" + "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.32.0" }, "funding": [ { @@ -3274,20 +3959,20 @@ "type": "tidelift" } ], - "time": "2024-09-09T11:45:10+00:00" + "time": "2024-12-23T08:48:59+00:00" }, { "name": "symfony/polyfill-php80", - "version": "v1.31.0", + "version": "v1.32.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-php80.git", - "reference": "60328e362d4c2c802a54fcbf04f9d3fb892b4cf8" + "reference": "0cc9dd0f17f61d8131e7df6b84bd344899fe2608" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/60328e362d4c2c802a54fcbf04f9d3fb892b4cf8", - "reference": "60328e362d4c2c802a54fcbf04f9d3fb892b4cf8", + "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/0cc9dd0f17f61d8131e7df6b84bd344899fe2608", + "reference": "0cc9dd0f17f61d8131e7df6b84bd344899fe2608", "shasum": "" }, "require": { @@ -3338,7 +4023,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-php80/tree/v1.31.0" + "source": "https://github.com/symfony/polyfill-php80/tree/v1.32.0" }, "funding": [ { @@ -3354,87 +4039,11 @@ "type": "tidelift" } ], - "time": "2024-09-09T11:45:10+00:00" - }, - { - "name": "symfony/polyfill-php81", - "version": "v1.31.0", - "source": { - "type": "git", - "url": "https://github.com/symfony/polyfill-php81.git", - "reference": "4a4cfc2d253c21a5ad0e53071df248ed48c6ce5c" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php81/zipball/4a4cfc2d253c21a5ad0e53071df248ed48c6ce5c", - "reference": "4a4cfc2d253c21a5ad0e53071df248ed48c6ce5c", - "shasum": "" - }, - "require": { - "php": ">=7.2" - }, - "type": "library", - "extra": { - "thanks": { - "name": "symfony/polyfill", - "url": "https://github.com/symfony/polyfill" - } - }, - "autoload": { - "files": [ - "bootstrap.php" - ], - "psr-4": { - "Symfony\\Polyfill\\Php81\\": "" - }, - "classmap": [ - "Resources/stubs" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Nicolas Grekas", - "email": "p@tchwork.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "Symfony polyfill backporting some PHP 8.1+ features to lower PHP versions", - "homepage": "https://symfony.com", - "keywords": [ - "compatibility", - "polyfill", - "portable", - "shim" - ], - "support": { - "source": "https://github.com/symfony/polyfill-php81/tree/v1.31.0" - }, - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], - "time": "2024-09-09T11:45:10+00:00" + "time": "2025-01-02T08:10:11+00:00" }, { "name": "symfony/polyfill-php83", - "version": "v1.31.0", + "version": "v1.32.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-php83.git", @@ -3452,8 +4061,8 @@ "type": "library", "extra": { "thanks": { - "name": "symfony/polyfill", - "url": "https://github.com/symfony/polyfill" + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" } }, "autoload": { @@ -3490,7 +4099,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-php83/tree/v1.31.0" + "source": "https://github.com/symfony/polyfill-php83/tree/v1.32.0" }, "funding": [ { @@ -3510,16 +4119,16 @@ }, { "name": "symfony/process", - "version": "v6.4.19", + "version": "v6.4.24", "source": { "type": "git", "url": "https://github.com/symfony/process.git", - "reference": "7a1c12e87b08ec9c97abdd188c9b3f5a40e37fc3" + "reference": "8eb6dc555bfb49b2703438d5de65cc9f138ff50b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/process/zipball/7a1c12e87b08ec9c97abdd188c9b3f5a40e37fc3", - "reference": "7a1c12e87b08ec9c97abdd188c9b3f5a40e37fc3", + "url": "https://api.github.com/repos/symfony/process/zipball/8eb6dc555bfb49b2703438d5de65cc9f138ff50b", + "reference": "8eb6dc555bfb49b2703438d5de65cc9f138ff50b", "shasum": "" }, "require": { @@ -3551,7 +4160,7 @@ "description": "Executes commands in sub-processes", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/process/tree/v6.4.19" + "source": "https://github.com/symfony/process/tree/v6.4.24" }, "funding": [ { @@ -3562,25 +4171,29 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2025-02-04T13:35:48+00:00" + "time": "2025-07-10T08:14:14+00:00" }, { "name": "symfony/service-contracts", - "version": "v3.5.1", + "version": "v3.6.0", "source": { "type": "git", "url": "https://github.com/symfony/service-contracts.git", - "reference": "e53260aabf78fb3d63f8d79d69ece59f80d5eda0" + "reference": "f021b05a130d35510bd6b25fe9053c2a8a15d5d4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/service-contracts/zipball/e53260aabf78fb3d63f8d79d69ece59f80d5eda0", - "reference": "e53260aabf78fb3d63f8d79d69ece59f80d5eda0", + "url": "https://api.github.com/repos/symfony/service-contracts/zipball/f021b05a130d35510bd6b25fe9053c2a8a15d5d4", + "reference": "f021b05a130d35510bd6b25fe9053c2a8a15d5d4", "shasum": "" }, "require": { @@ -3598,7 +4211,7 @@ "name": "symfony/contracts" }, "branch-alias": { - "dev-main": "3.5-dev" + "dev-main": "3.6-dev" } }, "autoload": { @@ -3634,7 +4247,7 @@ "standards" ], "support": { - "source": "https://github.com/symfony/service-contracts/tree/v3.5.1" + "source": "https://github.com/symfony/service-contracts/tree/v3.6.0" }, "funding": [ { @@ -3650,20 +4263,20 @@ "type": "tidelift" } ], - "time": "2024-09-25T14:20:29+00:00" + "time": "2025-04-25T09:37:31+00:00" }, { "name": "symfony/string", - "version": "v6.4.15", + "version": "v6.4.24", "source": { "type": "git", "url": "https://github.com/symfony/string.git", - "reference": "73a5e66ea2e1677c98d4449177c5a9cf9d8b4c6f" + "reference": "f0ce0bd36a3accb4a225435be077b4b4875587f4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/string/zipball/73a5e66ea2e1677c98d4449177c5a9cf9d8b4c6f", - "reference": "73a5e66ea2e1677c98d4449177c5a9cf9d8b4c6f", + "url": "https://api.github.com/repos/symfony/string/zipball/f0ce0bd36a3accb4a225435be077b4b4875587f4", + "reference": "f0ce0bd36a3accb4a225435be077b4b4875587f4", "shasum": "" }, "require": { @@ -3720,7 +4333,7 @@ "utf8" ], "support": { - "source": "https://github.com/symfony/string/tree/v6.4.15" + "source": "https://github.com/symfony/string/tree/v6.4.24" }, "funding": [ { @@ -3731,25 +4344,29 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2024-11-13T13:31:12+00:00" + "time": "2025-07-10T08:14:14+00:00" }, { "name": "symfony/translation-contracts", - "version": "v3.5.1", + "version": "v3.6.0", "source": { "type": "git", "url": "https://github.com/symfony/translation-contracts.git", - "reference": "4667ff3bd513750603a09c8dedbea942487fb07c" + "reference": "df210c7a2573f1913b2d17cc95f90f53a73d8f7d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/translation-contracts/zipball/4667ff3bd513750603a09c8dedbea942487fb07c", - "reference": "4667ff3bd513750603a09c8dedbea942487fb07c", + "url": "https://api.github.com/repos/symfony/translation-contracts/zipball/df210c7a2573f1913b2d17cc95f90f53a73d8f7d", + "reference": "df210c7a2573f1913b2d17cc95f90f53a73d8f7d", "shasum": "" }, "require": { @@ -3762,7 +4379,7 @@ "name": "symfony/contracts" }, "branch-alias": { - "dev-main": "3.5-dev" + "dev-main": "3.6-dev" } }, "autoload": { @@ -3798,7 +4415,7 @@ "standards" ], "support": { - "source": "https://github.com/symfony/translation-contracts/tree/v3.5.1" + "source": "https://github.com/symfony/translation-contracts/tree/v3.6.0" }, "funding": [ { @@ -3814,20 +4431,20 @@ "type": "tidelift" } ], - "time": "2024-09-25T14:20:29+00:00" + "time": "2024-09-27T08:32:26+00:00" }, { "name": "symfony/var-exporter", - "version": "v6.4.7", + "version": "v6.4.24", "source": { "type": "git", "url": "https://github.com/symfony/var-exporter.git", - "reference": "825f9b00c37bbe1c1691cc1aff9b5451fc9b4405" + "reference": "1e742d559fe5b19d0cdc281b1bf0b1fcc243bd35" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/var-exporter/zipball/825f9b00c37bbe1c1691cc1aff9b5451fc9b4405", - "reference": "825f9b00c37bbe1c1691cc1aff9b5451fc9b4405", + "url": "https://api.github.com/repos/symfony/var-exporter/zipball/1e742d559fe5b19d0cdc281b1bf0b1fcc243bd35", + "reference": "1e742d559fe5b19d0cdc281b1bf0b1fcc243bd35", "shasum": "" }, "require": { @@ -3875,7 +4492,7 @@ "serialize" ], "support": { - "source": "https://github.com/symfony/var-exporter/tree/v6.4.7" + "source": "https://github.com/symfony/var-exporter/tree/v6.4.24" }, "funding": [ { @@ -3886,12 +4503,16 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2024-04-18T09:22:46+00:00" + "time": "2025-07-10T08:14:14+00:00" }, { "name": "symfony/yaml", @@ -3967,26 +4588,26 @@ }, { "name": "twig/twig", - "version": "v3.14.0", + "version": "v3.21.1", "source": { "type": "git", "url": "https://github.com/twigphp/Twig.git", - "reference": "126b2c97818dbff0cdf3fbfc881aedb3d40aae72" + "reference": "285123877d4dd97dd7c11842ac5fb7e86e60d81d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/twigphp/Twig/zipball/126b2c97818dbff0cdf3fbfc881aedb3d40aae72", - "reference": "126b2c97818dbff0cdf3fbfc881aedb3d40aae72", + "url": "https://api.github.com/repos/twigphp/Twig/zipball/285123877d4dd97dd7c11842ac5fb7e86e60d81d", + "reference": "285123877d4dd97dd7c11842ac5fb7e86e60d81d", "shasum": "" }, "require": { - "php": ">=8.0.2", + "php": ">=8.1.0", "symfony/deprecation-contracts": "^2.5|^3", "symfony/polyfill-ctype": "^1.8", - "symfony/polyfill-mbstring": "^1.3", - "symfony/polyfill-php81": "^1.29" + "symfony/polyfill-mbstring": "^1.3" }, "require-dev": { + "phpstan/phpstan": "^2.0", "psr/container": "^1.0|^2.0", "symfony/phpunit-bridge": "^5.4.9|^6.4|^7.0" }, @@ -4030,7 +4651,7 @@ ], "support": { "issues": "https://github.com/twigphp/Twig/issues", - "source": "https://github.com/twigphp/Twig/tree/v3.14.0" + "source": "https://github.com/twigphp/Twig/tree/v3.21.1" }, "funding": [ { @@ -4042,7 +4663,7 @@ "type": "tidelift" } ], - "time": "2024-09-09T17:55:12+00:00" + "time": "2025-05-03T07:21:55+00:00" }, { "name": "webmozart/assert", diff --git a/packages/filesystem/src/FileSystem.php b/packages/filesystem/src/FileSystem.php index ceb322027..140f7b870 100644 --- a/packages/filesystem/src/FileSystem.php +++ b/packages/filesystem/src/FileSystem.php @@ -54,4 +54,6 @@ public function listContents(string $directory = '', bool $recursive = false): a /** @return StorageAttributes[] */ public function find(SpecificationInterface $specification): iterable; + + public function isDirectory(string $path): bool; } diff --git a/packages/filesystem/src/FlySystemAdapter.php b/packages/filesystem/src/FlySystemAdapter.php index 329187561..b01cecbe5 100644 --- a/packages/filesystem/src/FlySystemAdapter.php +++ b/packages/filesystem/src/FlySystemAdapter.php @@ -92,4 +92,9 @@ public function find(SpecificationInterface $specification): iterable { return $this->filesystem->find($specification); } + + public function isDirectory(string $path): bool + { + return $this->filesystem->isDirectory($path); + } } diff --git a/packages/filesystem/src/FlysystemV1/FlysystemV1.php b/packages/filesystem/src/FlysystemV1/FlysystemV1.php index 1a4698e6d..fae31ba85 100644 --- a/packages/filesystem/src/FlysystemV1/FlysystemV1.php +++ b/packages/filesystem/src/FlysystemV1/FlysystemV1.php @@ -75,4 +75,9 @@ public function find(SpecificationInterface $specification): iterable yield new \phpDocumentor\FileSystem\FlysystemV1\StorageAttributes($file); } } + + public function isDirectory(string $path): bool + { + return $this->filesystem->directoryExists($path); + } } diff --git a/packages/filesystem/src/FlysystemV3/FlysystemV3.php b/packages/filesystem/src/FlysystemV3/FlysystemV3.php index c5e9ce84a..be648914d 100644 --- a/packages/filesystem/src/FlysystemV3/FlysystemV3.php +++ b/packages/filesystem/src/FlysystemV3/FlysystemV3.php @@ -35,6 +35,11 @@ public function has(string $path): bool return $this->filesystem->has($path); } + public function isDirectory(string $path): bool + { + return $this->filesystem->directoryExists($path); + } + public function readStream(string $path): mixed { return $this->filesystem->readStream($path); diff --git a/packages/guides-cli/composer.json b/packages/guides-cli/composer.json index 09df05b5f..c03eff5ca 100644 --- a/packages/guides-cli/composer.json +++ b/packages/guides-cli/composer.json @@ -32,7 +32,10 @@ "symfony/config": "^5.4 || ^6.3 || ^7.0", "symfony/console": "^5.4 || ^6.3 || ^7.0", "symfony/dependency-injection": "^5.4 || ^6.3 || ^7.0", - "symfony/event-dispatcher": "^5.4 || ^6.3 || ^7.0" + "symfony/event-dispatcher": "^5.4 || ^6.3 || ^7.0", + "react/http": "^v1.11", + "react/socket": "^v1.16", + "league/mime-type-detection": "^1.16" }, "bin": [ "bin/guides" diff --git a/packages/guides-cli/resources/config/services.php b/packages/guides-cli/resources/config/services.php index e069a707c..4a1bb23ef 100644 --- a/packages/guides-cli/resources/config/services.php +++ b/packages/guides-cli/resources/config/services.php @@ -6,7 +6,11 @@ use phpDocumentor\Guides\Cli\Application; use phpDocumentor\Guides\Cli\Command\ProgressBarSubscriber; use phpDocumentor\Guides\Cli\Command\Run; +use phpDocumentor\Guides\Cli\Command\Serve; use phpDocumentor\Guides\Cli\Command\WorkingDirectorySwitcher; +use phpDocumentor\Guides\Cli\Command\SettingsBuilder; +use phpDocumentor\Guides\Cli\Internal\RunCommand; +use phpDocumentor\Guides\Cli\Internal\RunCommandHandler; use Psr\Clock\ClockInterface; use Psr\EventDispatcher\EventDispatcherInterface; use Psr\Log\LoggerInterface; @@ -26,6 +30,10 @@ ->public() ->tag('phpdoc.guides.cli.command') + ->set(Serve::class) + ->public() + ->tag('phpdoc.guides.cli.command') + ->set(NativeClock::class) ->alias(ClockInterface::class, NativeClock::class) @@ -44,5 +52,9 @@ ->set(WorkingDirectorySwitcher::class) ->tag('event_listener', ['event' => ConsoleEvents::COMMAND, 'method' => '__invoke']) - ->set(ProgressBarSubscriber::class); + ->set(ProgressBarSubscriber::class) + ->set(SettingsBuilder::class) + ->set(RunCommandHandler::class) + ->tag('phpdoc.guides.command', ['command' => RunCommand::class]) + ; }; diff --git a/packages/guides-cli/src/Command/Run.php b/packages/guides-cli/src/Command/Run.php index da7cff393..aee6b20fe 100644 --- a/packages/guides-cli/src/Command/Run.php +++ b/packages/guides-cli/src/Command/Run.php @@ -25,6 +25,7 @@ use Monolog\Logger; use phpDocumentor\FileSystem\Finder\Exclude; use phpDocumentor\FileSystem\FlySystemAdapter; +use phpDocumentor\Guides\Cli\Internal\RunCommand; use phpDocumentor\Guides\Cli\Logger\SpyProcessor; use phpDocumentor\Guides\Compiler\CompilerContext; use phpDocumentor\Guides\Event\PostProjectNodeCreated; @@ -71,47 +72,13 @@ public function __construct( private readonly ClockInterface $clock, private readonly EventDispatcher $eventDispatcher, private readonly ProgressBarSubscriber $progressBarSubscriber, + private SettingsBuilder $settingsBuilder, ) { parent::__construct('run'); - $this->addArgument( - 'input', - InputArgument::OPTIONAL, - 'Directory which holds the files to render', - ); - $this->addOption( - 'output', - null, - InputOption::VALUE_REQUIRED, - 'Directory to write rendered files to', - ); - - $this->addOption( - 'input-file', - null, - InputOption::VALUE_REQUIRED, - 'If set, only the specified file is parsed, relative to the directory specified in "input"', - ); - - $this->addOption( - 'exclude-path', - null, - InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY, - 'Paths to exclude, doc files in these directories will not be parsed', - ); + $this->settingsBuilder ??= new SettingsBuilder($this->eventDispatcher, $this->settingsManager, $this->clock); + $this->settingsBuilder->configureCommand($this); - $this->addOption( - 'input-format', - null, - InputOption::VALUE_REQUIRED, - 'Format of the input can be "RST", or "Markdown"', - ); - $this->addOption( - 'output-format', - null, - InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY, - 'Format of the input can be "html" and/or "interlink"', - ); $this->addOption( 'log-path', null, @@ -132,13 +99,6 @@ public function __construct( 'If set, returns a non-zero exit code as soon as any errors occur', ); - $this->addOption( - 'theme', - null, - InputOption::VALUE_REQUIRED, - 'The theme used for rendering', - ); - $this->addOption( 'progress', null, @@ -159,93 +119,11 @@ public function registerProgressBar(ConsoleOutputInterface $output): void $this->progressBarSubscriber->subscribe($output, $this->eventDispatcher); } - private function getSettingsOverriddenWithInput(InputInterface $input): ProjectSettings - { - $settings = $this->settingsManager->getProjectSettings(); - - if ($settings->isShowProgressBar()) { - $settings->setShowProgressBar($input->getOption('progress')); - } - - if ($input->getArgument('input')) { - $settings->setInput((string) $input->getArgument('input')); - } - - if ($input->getOption('output')) { - $settings->setOutput((string) $input->getOption('output')); - } - - if ($input->getOption('input-file')) { - $inputFile = (string) $input->getOption('input-file'); - $pathInfo = pathinfo($inputFile); - $settings->setInputFile($pathInfo['filename']); - if (!empty($pathInfo['extension'])) { - $settings->setInputFormat($pathInfo['extension']); - } - } - - if ($input->getOption('input-format')) { - $settings->setInputFormat((string) $input->getOption('input-format')); - } - - if ($input->getOption('log-path')) { - $settings->setLogPath((string) $input->getOption('log-path')); - } - - if ($input->getOption('fail-on-error')) { - $settings->setFailOnError(LogLevel::ERROR); - } - - if ($input->getOption('fail-on-log')) { - $settings->setFailOnError(LogLevel::WARNING); - } - - if (count($input->getOption('output-format')) > 0) { - $settings->setOutputFormats($input->getOption('output-format')); - } - - if ($input->getOption('theme')) { - $settings->setTheme((string) $input->getOption('theme')); - } - - if (method_exists($settings, 'setExcludes')) { - /** @var list $excludePaths */ - $excludePaths = (array) $input->getOption('exclude-path'); - if ($excludePaths !== []) { - $settings->setExcludes( - $settings->getExcludes()->withPaths($excludePaths), - ); - } - } - - return $settings; - } - protected function execute(InputInterface $input, OutputInterface $output): int { - $settings = $this->getSettingsOverriddenWithInput($input); - $inputDir = $settings->getInput(); - if (!is_dir($inputDir)) { - throw new RuntimeException(sprintf('Input directory "%s" was not found! ' . "\n" . - 'Run "vendor/bin/guides -h" for information on how to configure this command.', $inputDir)); - } - - $projectNode = new ProjectNode( - $settings->getTitle() === '' ? null : $settings->getTitle(), - $settings->getVersion() === '' ? null : $settings->getVersion(), - $settings->getRelease() === '' ? null : $settings->getRelease(), - $settings->getCopyright() === '' ? null : $settings->getCopyright(), - $this->clock->now(), - ); - - $event = new PostProjectNodeCreated($projectNode, $settings); - $event = $this->eventDispatcher->dispatch($event); - assert($event instanceof PostProjectNodeCreated); - $projectNode = $event->getProjectNode(); - $settings = $event->getSettings(); - - $outputDir = $settings->getOutput(); - $sourceFileSystem = FlySystemAdapter::createForPath($settings->getInput()); + $this->settingsBuilder->overrideWithInput($input); + $projectNode = $this->settingsBuilder->createProjectNode(); + $settings = $this->settingsBuilder->getSettings(); $logPath = $settings->getLogPath(); if ($logPath === 'php://stder') { @@ -260,57 +138,16 @@ protected function execute(InputInterface $input, OutputInterface $output): int $this->logger->pushProcessor($spyProcessor); } - $documents = []; - - if ($output instanceof ConsoleOutputInterface && $settings->isShowProgressBar()) { $this->progressBarSubscriber->subscribe($output, $this->eventDispatcher); } - if ($settings->getInputFile() === '') { - $documents = $this->commandBus->handle( - new ParseDirectoryCommand( - $sourceFileSystem, - '', - $settings->getInputFormat(), - $projectNode, - $this->getExclude($settings, $input), - ), - ); - } else { - $documents[] = $this->commandBus->handle( - new ParseFileCommand( - $sourceFileSystem, - '', - $settings->getInputFile(), - $settings->getInputFormat(), - 1, - $projectNode, - true, - ), - ); - } - - $this->themeManager->useTheme($settings->getTheme()); - - $documents = $this->commandBus->handle(new CompileDocumentsCommand($documents, new CompilerContext($projectNode))); - - $destinationFileSystem = FlySystemAdapter::createForPath($outputDir); + $documents = $this->commandBus->handle( + new RunCommand($settings, $projectNode, $input), + ); $outputFormats = $settings->getOutputFormats(); - - foreach ($outputFormats as $format) { - $this->commandBus->handle( - new RenderCommand( - $format, - $documents, - $sourceFileSystem, - $destinationFileSystem, - $projectNode, - ), - ); - } - + $outputDir = $settings->getOutput(); if ($output->isQuiet() === false) { $lastFormat = ''; @@ -331,31 +168,4 @@ protected function execute(InputInterface $input, OutputInterface $output): int return Command::SUCCESS; } - - private function getExclude(ProjectSettings $settings, InputInterface|null $input = null): Exclude|SpecificationInterface|null - { - if (method_exists($settings, 'getExcludes')) { - return $settings->getExcludes(); - } - - if ($input === null) { - return null; - } - - if ($input->getOption('exclude-path')) { - /** @var string[] $excludedPaths */ - $excludedPaths = (array) $input->getOption('exclude-path'); - $excludedSpecifications = array_map(static fn (string $path) => new NotSpecification(new InPath(new Path($path))), $excludedPaths); - $excludedSpecification = array_shift($excludedSpecifications); - assert($excludedSpecification !== null); - - return array_reduce( - $excludedSpecifications, - static fn (SpecificationInterface $carry, SpecificationInterface $spec) => new OrSpecification($carry, $spec), - $excludedSpecification, - ); - } - - return null; - } } diff --git a/packages/guides-cli/src/Command/Serve.php b/packages/guides-cli/src/Command/Serve.php new file mode 100644 index 000000000..5ff3febeb --- /dev/null +++ b/packages/guides-cli/src/Command/Serve.php @@ -0,0 +1,165 @@ +settingsBuilder->configureCommand($this); + } + + protected function execute(InputInterface $input, OutputInterface $output): int + { + // Enable tick processing for signal handling + declare(ticks=1); + + $loop = Loop::get(); + + $watcher = new InotifyWatcher($loop, $this->dispatcher, $input->getArgument('input')); + + $this->dispatcher->addListener( + PostParseDocument::class, + static function (PostParseDocument $event) use ($watcher): void { + $watcher->addPath($event->getOriginalFileName()); + }, + ); + + $this->dispatcher->addListener( + FileModifiedEvent::class, + function (FileModifiedEvent $event) use ($input, $output): void { + $output->writeln( + sprintf( + 'File modified: %s, rerendering...', + $event->path, + ), + ); + $this->commandBus->handle( + new RunCommand( + $this->settingsBuilder->getSettings(), + $this->settingsBuilder->createProjectNode(), + $input, + ), + ); + $output->writeln('Rerendering completed.'); + }, + ); + + $this->settingsBuilder->overrideWithInput($input); + + $this->commandBus->handle( + new RunCommand( + $this->settingsBuilder->getSettings(), + $this->settingsBuilder->createProjectNode(), + $input, + ), + ); + + + $dir = $input->getOption('output'); + if ($dir === null) { + $output->writeln('Please specify an output directory using --output option'); + + return Command::FAILURE; + } + + $files = FlySystemAdapter::createForPath($dir); + + $http = new HttpServer(static function (ServerRequestInterface $request) use ($output, $files) { + $output->writeln( + sprintf( + 'Received request for %s from %s', + $request->getUri(), + $request->getServerParams()['REMOTE_ADDR'] ?? 'unknown', + ), + ); + + $requestPath = trim($request->getUri()->getPath(), '/'); + + $output->writeln(sprintf( + 'Request path: %s', + $requestPath, + )); + + $detector = new ExtensionMimeTypeDetector(); + if ($files->isDirectory($requestPath)) { + $requestPath .= '/index.html'; + } + + if ($files->has($requestPath)) { + return Response::html( + $files->read($requestPath) ?: '', + )->withHeader( + 'Content-Type', + $detector->detectMimeTypeFromPath($requestPath) ?? 'text/plain', + ); + } + + return Response::html( + "page not found!\n", + )->withStatus(404); + }); + + $socket = new SocketServer('0.0.0.0:1337', [], $loop); + $http->listen($socket); + + $output->writeln(sprintf('Server running at http://127.0.0.1:1337')); + $output->writeln('Press Ctrl+C to stop the server'); + + // Handle SIGINT (Ctrl+C) gracefully if PCNTL extension is available + if (function_exists('pcntl_signal')) { + // 2 is the signal number for SIGINT (Ctrl+C) + pcntl_signal(2, static function () use ($loop, $socket, $output): void { + $output->writeln('Shutting down server...'); + $socket->close(); + $loop->stop(); + $output->writeln('Server stopped'); + exit(0); + }); + } else { + $output->writeln('Note: PCNTL extension not available, Ctrl+C handling may not work properly'); + } + + // Create a periodic timer to ensure the loop regularly processes events + // This helps in processing signals even when there are no other events + $loop->addPeriodicTimer(0.5, static function (): void { + // This empty callback just ensures the loop wakes up regularly + // which improves the responsiveness to signals + }); + + $loop->run(); + + return Command::SUCCESS; + } +} diff --git a/packages/guides-cli/src/Command/SettingsBuilder.php b/packages/guides-cli/src/Command/SettingsBuilder.php new file mode 100644 index 000000000..e44771647 --- /dev/null +++ b/packages/guides-cli/src/Command/SettingsBuilder.php @@ -0,0 +1,180 @@ +settingsManager->getProjectSettings(); + + if ($settings->isShowProgressBar() && $input->hasOption('progress')) { + $settings->setShowProgressBar($input->getOption('progress')); + } + + if ($input->getArgument('input')) { + $settings->setInput((string) $input->getArgument('input')); + } + + if ($input->getOption('output')) { + $settings->setOutput((string) $input->getOption('output')); + } + + if ($input->getOption('input-file')) { + $inputFile = (string) $input->getOption('input-file'); + $pathInfo = pathinfo($inputFile); + $settings->setInputFile($pathInfo['filename']); + if (!empty($pathInfo['extension'])) { + $settings->setInputFormat($pathInfo['extension']); + } + } + + if ($input->getOption('input-format')) { + $settings->setInputFormat((string) $input->getOption('input-format')); + } + + if ($input->hasOption('log-path')) { + $settings->setLogPath((string) $input->getOption('log-path')); + } + + if ($input->hasOption('fail-on-error')) { + $settings->setFailOnError(LogLevel::ERROR); + } + + if ($input->hasOption('fail-on-log')) { + $settings->setFailOnError(LogLevel::WARNING); + } + + if (count($input->getOption('output-format')) > 0) { + $settings->setOutputFormats($input->getOption('output-format')); + } + + if ($input->getOption('theme')) { + $settings->setTheme((string) $input->getOption('theme')); + } + + if (method_exists($settings, 'setExcludes')) { + /** @var list $excludePaths */ + $excludePaths = (array) $input->getOption('exclude-path'); + if ($excludePaths !== []) { + $settings->setExcludes( + $settings->getExcludes()->withPaths($excludePaths), + ); + } + } + + $this->settings = $settings; + } + + public function createProjectNode(): ProjectNode + { + $projectNode = new ProjectNode( + $this->settings->getTitle() === '' ? null : $this->settings->getTitle(), + $this->settings->getVersion() === '' ? null : $this->settings->getVersion(), + $this->settings->getRelease() === '' ? null : $this->settings->getRelease(), + $this->settings->getCopyright() === '' ? null : $this->settings->getCopyright(), + $this->clock->now(), + ); + + $event = new PostProjectNodeCreated($projectNode, $this->settings); + $event = $this->eventDispatcher->dispatch($event); + assert($event instanceof PostProjectNodeCreated); + $projectNode = $event->getProjectNode(); + $this->settings = $event->getSettings(); + + return $projectNode; + } + + public function getSettings(): ProjectSettings + { + $inputDir = $this->settings->getInput(); + if (!is_dir($inputDir)) { + throw new RuntimeException(sprintf('Input directory "%s" was not found! ' . "\n" . + 'Run "vendor/bin/guides -h" for information on how to configure this command.', $inputDir)); + } + + return $this->settings; + } + + public function configureCommand(Command $command): void + { + $command->addArgument( + 'input', + InputArgument::OPTIONAL, + 'Directory which holds the files to render', + ); + $command->addOption( + 'output', + null, + InputOption::VALUE_REQUIRED, + 'Directory to write rendered files to', + ); + + $command->addOption( + 'input-file', + null, + InputOption::VALUE_REQUIRED, + 'If set, only the specified file is parsed, relative to the directory specified in "input"', + ); + + $command->addOption( + 'exclude-path', + null, + InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY, + 'Paths to exclude, doc files in these directories will not be parsed', + ); + + $command->addOption( + 'input-format', + null, + InputOption::VALUE_REQUIRED, + 'Format of the input can be "RST", or "Markdown"', + ); + $command->addOption( + 'output-format', + null, + InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY, + 'Format of the input can be "html" and/or "interlink"', + ); + + $command->addOption( + 'theme', + null, + InputOption::VALUE_REQUIRED, + 'The theme used for rendering', + ); + + } +} diff --git a/packages/guides-cli/src/Internal/RunCommand.php b/packages/guides-cli/src/Internal/RunCommand.php new file mode 100644 index 000000000..9ffa63fb9 --- /dev/null +++ b/packages/guides-cli/src/Internal/RunCommand.php @@ -0,0 +1,20 @@ +settings; + $projectNode = $command->projectNode; + $outputDir = $settings->getOutput(); + $sourceFileSystem = FlySystemAdapter::createForPath($settings->getInput()); + $documents = []; + if ($settings->getInputFile() === '') { + $documents = $this->commandBus->handle( + new ParseDirectoryCommand( + $sourceFileSystem, + '', + $settings->getInputFormat(), + $projectNode, + $this->getExclude($settings, $command->input), + ), + ); + } else { + $documents[] = $this->commandBus->handle( + new ParseFileCommand( + $sourceFileSystem, + '', + $settings->getInputFile(), + $settings->getInputFormat(), + 1, + $projectNode, + true, + ), + ); + } + + $this->themeManager->useTheme($settings->getTheme()); + + $documents = $this->commandBus->handle(new CompileDocumentsCommand($documents, new CompilerContext($projectNode))); + + $destinationFileSystem = FlySystemAdapter::createForPath($outputDir); + + $outputFormats = $settings->getOutputFormats(); + + foreach ($outputFormats as $format) { + $this->commandBus->handle( + new RenderCommand( + $format, + $documents, + $sourceFileSystem, + $destinationFileSystem, + $projectNode, + ), + ); + } + + return $documents; + } + + private function getExclude(ProjectSettings $settings, InputInterface|null $input = null): Exclude|SpecificationInterface|null + { + if (method_exists($settings, 'getExcludes')) { + return $settings->getExcludes(); + } + + if ($input === null) { + return null; + } + + if ($input->getOption('exclude-path')) { + /** @var string[] $excludedPaths */ + $excludedPaths = (array) $input->getOption('exclude-path'); + $excludedSpecifications = array_map(static fn (string $path) => new NotSpecification(new InPath(new Path($path))), $excludedPaths); + $excludedSpecification = array_shift($excludedSpecifications); + assert($excludedSpecification !== null); + + return array_reduce( + $excludedSpecifications, + static fn (SpecificationInterface $carry, SpecificationInterface $spec) => new OrSpecification($carry, $spec), + $excludedSpecification, + ); + } + + return null; + } +} diff --git a/packages/guides-cli/src/Watcher/FileModifiedEvent.php b/packages/guides-cli/src/Watcher/FileModifiedEvent.php new file mode 100644 index 000000000..7e402ba90 --- /dev/null +++ b/packages/guides-cli/src/Watcher/FileModifiedEvent.php @@ -0,0 +1,12 @@ + */ + private array $watchDescriptors; + + public function __construct( + private LoopInterface $loop, + private EventDispatcherInterface $dispatcher, + private string $inputPath, + ) { + } + + public function __invoke(): void + { + if (($events = inotify_read($this->inotify)) === false) { + return; + } + + foreach ($events as $event) { + if (!isset($this->watchDescriptors[$event['wd']])) { + continue; + } + + $path = $this->watchDescriptors[$event['wd']]['path']; + switch ($event['mask']) { + case IN_MODIFY: + $this->dispatcher->dispatch(new FileModifiedEvent($path)); + break; + case IN_CREATE: + //$this->dispatcher->dispatch(new FileCreatedEvent($path, $event['name'])); + break; + case IN_DELETE: + //$this->dispatcher->dispatch(new FileDeletedEvent($path, $event['name'])); + break; + default: + var_dump('Unhandled event mask: ' . $event['mask']); + } + } + } + + public function addPath(string $path): void + { + if ($this->inotify === false) { + $this->inotify = inotify_init(); + stream_set_blocking($this->inotify, false); + + // wait for any file events by reading from inotify handler asynchronously + $this->loop->addReadStream($this->inotify, $this); + } + + $descriptor = inotify_add_watch($this->inotify, $this->inputPath . DIRECTORY_SEPARATOR . $path, IN_MODIFY | IN_CREATE | IN_DELETE); + $this->watchDescriptors[$descriptor] = ['path' => $path]; + } +} From 2564970a01f81423437e9d7ebd31c57fd3d01b3b Mon Sep 17 00:00:00 2001 From: Jaapio Date: Mon, 25 Aug 2025 22:16:51 +0200 Subject: [PATCH 2/4] Implement websocket handling --- composer.lock | 451 +++++++++++++++++- packages/guides-cli/composer.json | 1 + packages/guides-cli/src/Command/Serve.php | 87 ++-- .../guides-cli/src/Internal/HttpHandler.php | 126 +++++ .../src/Internal/UpdatePageServer.php | 52 ++ .../guides-cli/src/Watcher/INotifyWatcher.php | 43 +- 6 files changed, 693 insertions(+), 67 deletions(-) create mode 100644 packages/guides-cli/src/Internal/HttpHandler.php create mode 100644 packages/guides-cli/src/Internal/UpdatePageServer.php diff --git a/composer.lock b/composer.lock index c598534f7..ec34e1807 100644 --- a/composer.lock +++ b/composer.lock @@ -6,6 +6,69 @@ ], "content-hash": "36cc19c043cd4e53ba6d7cff20e23b9d", "packages": [ + { + "name": "cboden/ratchet", + "version": "v0.4.4", + "source": { + "type": "git", + "url": "https://github.com/ratchetphp/Ratchet.git", + "reference": "5012dc954541b40c5599d286fd40653f5716a38f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/ratchetphp/Ratchet/zipball/5012dc954541b40c5599d286fd40653f5716a38f", + "reference": "5012dc954541b40c5599d286fd40653f5716a38f", + "shasum": "" + }, + "require": { + "guzzlehttp/psr7": "^1.7|^2.0", + "php": ">=5.4.2", + "ratchet/rfc6455": "^0.3.1", + "react/event-loop": ">=0.4", + "react/socket": "^1.0 || ^0.8 || ^0.7 || ^0.6 || ^0.5", + "symfony/http-foundation": "^2.6|^3.0|^4.0|^5.0|^6.0", + "symfony/routing": "^2.6|^3.0|^4.0|^5.0|^6.0" + }, + "require-dev": { + "phpunit/phpunit": "~4.8" + }, + "type": "library", + "autoload": { + "psr-4": { + "Ratchet\\": "src/Ratchet" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Chris Boden", + "email": "cboden@gmail.com", + "role": "Developer" + }, + { + "name": "Matt Bonneau", + "role": "Developer" + } + ], + "description": "PHP WebSocket library", + "homepage": "http://socketo.me", + "keywords": [ + "Ratchet", + "WebSockets", + "server", + "sockets", + "websocket" + ], + "support": { + "chat": "https://gitter.im/reactphp/reactphp", + "issues": "https://github.com/ratchetphp/Ratchet/issues", + "source": "https://github.com/ratchetphp/Ratchet/tree/v0.4.4" + }, + "time": "2021-12-14T00:20:41+00:00" + }, { "name": "dflydev/dot-access-data", "version": "v3.0.2", @@ -309,6 +372,122 @@ }, "time": "2020-11-24T22:02:12+00:00" }, + { + "name": "guzzlehttp/psr7", + "version": "2.8.0", + "source": { + "type": "git", + "url": "https://github.com/guzzle/psr7.git", + "reference": "21dc724a0583619cd1652f673303492272778051" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/psr7/zipball/21dc724a0583619cd1652f673303492272778051", + "reference": "21dc724a0583619cd1652f673303492272778051", + "shasum": "" + }, + "require": { + "php": "^7.2.5 || ^8.0", + "psr/http-factory": "^1.0", + "psr/http-message": "^1.1 || ^2.0", + "ralouphie/getallheaders": "^3.0" + }, + "provide": { + "psr/http-factory-implementation": "1.0", + "psr/http-message-implementation": "1.0" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.8.2", + "http-interop/http-factory-tests": "0.9.0", + "phpunit/phpunit": "^8.5.44 || ^9.6.25" + }, + "suggest": { + "laminas/laminas-httphandlerrunner": "Emit PSR-7 responses" + }, + "type": "library", + "extra": { + "bamarni-bin": { + "bin-links": true, + "forward-command": false + } + }, + "autoload": { + "psr-4": { + "GuzzleHttp\\Psr7\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + }, + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + }, + { + "name": "George Mponos", + "email": "gmponos@gmail.com", + "homepage": "https://github.com/gmponos" + }, + { + "name": "Tobias Nyholm", + "email": "tobias.nyholm@gmail.com", + "homepage": "https://github.com/Nyholm" + }, + { + "name": "Márk Sági-Kazár", + "email": "mark.sagikazar@gmail.com", + "homepage": "https://github.com/sagikazarmark" + }, + { + "name": "Tobias Schultze", + "email": "webmaster@tubo-world.de", + "homepage": "https://github.com/Tobion" + }, + { + "name": "Márk Sági-Kazár", + "email": "mark.sagikazar@gmail.com", + "homepage": "https://sagikazarmark.hu" + } + ], + "description": "PSR-7 message implementation that also provides common utility methods", + "keywords": [ + "http", + "message", + "psr-7", + "request", + "response", + "stream", + "uri", + "url" + ], + "support": { + "issues": "https://github.com/guzzle/psr7/issues", + "source": "https://github.com/guzzle/psr7/tree/2.8.0" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://github.com/Nyholm", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/guzzlehttp/psr7", + "type": "tidelift" + } + ], + "time": "2025-08-23T21:21:41+00:00" + }, { "name": "jawira/plantuml-encoding", "version": "v1.1.0", @@ -1537,9 +1716,10 @@ "dist": { "type": "path", "url": "./packages/guides-cli", - "reference": "92668cc362e81169056a7897e8444e1ef62ea030" + "reference": "5eca460a69b7fb112d2b008bfeabd9c189955aee" }, "require": { + "cboden/ratchet": "^0.4.4", "league/mime-type-detection": "^1.16", "monolog/monolog": "^3.0", "php": "^8.1", @@ -2137,6 +2317,107 @@ }, "time": "2024-09-11T13:17:53+00:00" }, + { + "name": "ralouphie/getallheaders", + "version": "3.0.3", + "source": { + "type": "git", + "url": "https://github.com/ralouphie/getallheaders.git", + "reference": "120b605dfeb996808c31b6477290a714d356e822" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/ralouphie/getallheaders/zipball/120b605dfeb996808c31b6477290a714d356e822", + "reference": "120b605dfeb996808c31b6477290a714d356e822", + "shasum": "" + }, + "require": { + "php": ">=5.6" + }, + "require-dev": { + "php-coveralls/php-coveralls": "^2.1", + "phpunit/phpunit": "^5 || ^6.5" + }, + "type": "library", + "autoload": { + "files": [ + "src/getallheaders.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ralph Khattar", + "email": "ralph.khattar@gmail.com" + } + ], + "description": "A polyfill for getallheaders.", + "support": { + "issues": "https://github.com/ralouphie/getallheaders/issues", + "source": "https://github.com/ralouphie/getallheaders/tree/develop" + }, + "time": "2019-03-08T08:55:37+00:00" + }, + { + "name": "ratchet/rfc6455", + "version": "v0.3.1", + "source": { + "type": "git", + "url": "https://github.com/ratchetphp/RFC6455.git", + "reference": "7c964514e93456a52a99a20fcfa0de242a43ccdb" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/ratchetphp/RFC6455/zipball/7c964514e93456a52a99a20fcfa0de242a43ccdb", + "reference": "7c964514e93456a52a99a20fcfa0de242a43ccdb", + "shasum": "" + }, + "require": { + "guzzlehttp/psr7": "^2 || ^1.7", + "php": ">=5.4.2" + }, + "require-dev": { + "phpunit/phpunit": "^5.7", + "react/socket": "^1.3" + }, + "type": "library", + "autoload": { + "psr-4": { + "Ratchet\\RFC6455\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Chris Boden", + "email": "cboden@gmail.com", + "role": "Developer" + }, + { + "name": "Matt Bonneau", + "role": "Developer" + } + ], + "description": "RFC6455 WebSocket protocol handler", + "homepage": "http://socketo.me", + "keywords": [ + "WebSockets", + "rfc6455", + "websocket" + ], + "support": { + "chat": "https://gitter.im/reactphp/reactphp", + "issues": "https://github.com/ratchetphp/RFC6455/issues", + "source": "https://github.com/ratchetphp/RFC6455/tree/v0.3.1" + }, + "time": "2021-12-09T23:20:49+00:00" + }, { "name": "react/cache", "version": "v1.2.0", @@ -3642,6 +3923,87 @@ ], "time": "2025-04-29T11:18:49+00:00" }, + { + "name": "symfony/http-foundation", + "version": "v6.4.24", + "source": { + "type": "git", + "url": "https://github.com/symfony/http-foundation.git", + "reference": "0341e41d8d8830c31a1dff5cbc5bdb3ec872a073" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/http-foundation/zipball/0341e41d8d8830c31a1dff5cbc5bdb3ec872a073", + "reference": "0341e41d8d8830c31a1dff5cbc5bdb3ec872a073", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/polyfill-mbstring": "~1.1", + "symfony/polyfill-php83": "^1.27" + }, + "conflict": { + "symfony/cache": "<6.4.12|>=7.0,<7.1.5" + }, + "require-dev": { + "doctrine/dbal": "^2.13.1|^3|^4", + "predis/predis": "^1.1|^2.0", + "symfony/cache": "^6.4.12|^7.1.5", + "symfony/dependency-injection": "^5.4|^6.0|^7.0", + "symfony/expression-language": "^5.4|^6.0|^7.0", + "symfony/http-kernel": "^5.4.12|^6.0.12|^6.1.4|^7.0", + "symfony/mime": "^5.4|^6.0|^7.0", + "symfony/rate-limiter": "^5.4|^6.0|^7.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\HttpFoundation\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Defines an object-oriented layer for the HTTP specification", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/http-foundation/tree/v6.4.24" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-07-10T08:14:14+00:00" + }, { "name": "symfony/polyfill-ctype", "version": "v1.32.0", @@ -4182,6 +4544,93 @@ ], "time": "2025-07-10T08:14:14+00:00" }, + { + "name": "symfony/routing", + "version": "v6.4.24", + "source": { + "type": "git", + "url": "https://github.com/symfony/routing.git", + "reference": "e4f94e625c8e6f910aa004a0042f7b2d398278f5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/routing/zipball/e4f94e625c8e6f910aa004a0042f7b2d398278f5", + "reference": "e4f94e625c8e6f910aa004a0042f7b2d398278f5", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "symfony/deprecation-contracts": "^2.5|^3" + }, + "conflict": { + "doctrine/annotations": "<1.12", + "symfony/config": "<6.2", + "symfony/dependency-injection": "<5.4", + "symfony/yaml": "<5.4" + }, + "require-dev": { + "doctrine/annotations": "^1.12|^2", + "psr/log": "^1|^2|^3", + "symfony/config": "^6.2|^7.0", + "symfony/dependency-injection": "^5.4|^6.0|^7.0", + "symfony/expression-language": "^5.4|^6.0|^7.0", + "symfony/http-foundation": "^5.4|^6.0|^7.0", + "symfony/yaml": "^5.4|^6.0|^7.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Routing\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Maps an HTTP request to a set of configuration variables", + "homepage": "https://symfony.com", + "keywords": [ + "router", + "routing", + "uri", + "url" + ], + "support": { + "source": "https://github.com/symfony/routing/tree/v6.4.24" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-07-15T08:46:37+00:00" + }, { "name": "symfony/service-contracts", "version": "v3.6.0", diff --git a/packages/guides-cli/composer.json b/packages/guides-cli/composer.json index c03eff5ca..d08320f7a 100644 --- a/packages/guides-cli/composer.json +++ b/packages/guides-cli/composer.json @@ -33,6 +33,7 @@ "symfony/console": "^5.4 || ^6.3 || ^7.0", "symfony/dependency-injection": "^5.4 || ^6.3 || ^7.0", "symfony/event-dispatcher": "^5.4 || ^6.3 || ^7.0", + "cboden/ratchet": "^0.4.4", "react/http": "^v1.11", "react/socket": "^v1.16", "league/mime-type-detection": "^1.16" diff --git a/packages/guides-cli/src/Command/Serve.php b/packages/guides-cli/src/Command/Serve.php index 5ff3febeb..52039d17b 100644 --- a/packages/guides-cli/src/Command/Serve.php +++ b/packages/guides-cli/src/Command/Serve.php @@ -4,35 +4,36 @@ namespace phpDocumentor\Guides\Cli\Command; -use League\MimeTypeDetection\ExtensionMimeTypeDetector; use League\Tactician\CommandBus; use phpDocumentor\FileSystem\FlySystemAdapter; +use phpDocumentor\Guides\Cli\Internal\HttpHandler; use phpDocumentor\Guides\Cli\Internal\RunCommand; +use phpDocumentor\Guides\Cli\Internal\UpdatePageServer; use phpDocumentor\Guides\Cli\Watcher\FileModifiedEvent; use phpDocumentor\Guides\Cli\Watcher\INotifyWatcher; use phpDocumentor\Guides\Event\PostParseDocument; use Psr\EventDispatcher\EventDispatcherInterface; -use Psr\Http\Message\ServerRequestInterface; +use Ratchet\App; use React\EventLoop\Loop; -use React\Http\HttpServer; -use React\Http\Message\Response; -use React\Socket\SocketServer; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; +use Symfony\Component\Routing\Route; use function function_exists; use function sprintf; -use function trim; final class Serve extends Command { + private UpdatePageServer $wsServer; + public function __construct( private EventDispatcherInterface $dispatcher, private SettingsBuilder $settingsBuilder, private CommandBus $commandBus, ) { parent::__construct('serve'); + $this->wsServer = new UpdatePageServer(); } protected function configure(): void @@ -73,6 +74,9 @@ function (FileModifiedEvent $event) use ($input, $output): void { ), ); $output->writeln('Rerendering completed.'); + + // Notify connected clients that they should reload + $this->wsServer->sendUpdate(); }, ); @@ -86,7 +90,6 @@ function (FileModifiedEvent $event) use ($input, $output): void { ), ); - $dir = $input->getOption('output'); if ($dir === null) { $output->writeln('Please specify an output directory using --output option'); @@ -96,53 +99,38 @@ function (FileModifiedEvent $event) use ($input, $output): void { $files = FlySystemAdapter::createForPath($dir); - $http = new HttpServer(static function (ServerRequestInterface $request) use ($output, $files) { - $output->writeln( - sprintf( - 'Received request for %s from %s', - $request->getUri(), - $request->getServerParams()['REMOTE_ADDR'] ?? 'unknown', - ), - ); - - $requestPath = trim($request->getUri()->getPath(), '/'); - - $output->writeln(sprintf( - 'Request path: %s', - $requestPath, - )); - - $detector = new ExtensionMimeTypeDetector(); - if ($files->isDirectory($requestPath)) { - $requestPath .= '/index.html'; - } - - if ($files->has($requestPath)) { - return Response::html( - $files->read($requestPath) ?: '', - )->withHeader( - 'Content-Type', - $detector->detectMimeTypeFromPath($requestPath) ?? 'text/plain', - ); - } + // Create HTTP handler for serving files + $httpHandler = new HttpHandler($output, $files); - return Response::html( - "page not found!\n", - )->withStatus(404); - }); + // Setup the Ratchet App with routes + $app = new App('localhost', 1337, '0.0.0.0', $loop); - $socket = new SocketServer('0.0.0.0:1337', [], $loop); - $http->listen($socket); + // Add WebSocket route at /ws + $app->route('/ws', $this->wsServer, ['*']); - $output->writeln(sprintf('Server running at http://127.0.0.1:1337')); + // Add root path first + $app->route('/', $httpHandler, ['*']); + + // Add HTTP server for all other routes - use a different pattern syntax + $app->routes->add('catch-all', new Route( + '/{url}', + ['_controller' => $httpHandler], + ['url' => '.+'], + [], + 'localhost', + [], + ['GET'], + )); + + $output->writeln(sprintf('Server running at http://localhost:1337')); + $output->writeln('WebSocket server running at ws://localhost:1337/ws'); $output->writeln('Press Ctrl+C to stop the server'); // Handle SIGINT (Ctrl+C) gracefully if PCNTL extension is available if (function_exists('pcntl_signal')) { // 2 is the signal number for SIGINT (Ctrl+C) - pcntl_signal(2, static function () use ($loop, $socket, $output): void { + pcntl_signal(2, static function () use ($loop, $output): void { $output->writeln('Shutting down server...'); - $socket->close(); $loop->stop(); $output->writeln('Server stopped'); exit(0); @@ -151,14 +139,7 @@ function (FileModifiedEvent $event) use ($input, $output): void { $output->writeln('Note: PCNTL extension not available, Ctrl+C handling may not work properly'); } - // Create a periodic timer to ensure the loop regularly processes events - // This helps in processing signals even when there are no other events - $loop->addPeriodicTimer(0.5, static function (): void { - // This empty callback just ensures the loop wakes up regularly - // which improves the responsiveness to signals - }); - - $loop->run(); + $app->run(); return Command::SUCCESS; } diff --git a/packages/guides-cli/src/Internal/HttpHandler.php b/packages/guides-cli/src/Internal/HttpHandler.php new file mode 100644 index 000000000..8968d2d5c --- /dev/null +++ b/packages/guides-cli/src/Internal/HttpHandler.php @@ -0,0 +1,126 @@ +detector = new ExtensionMimeTypeDetector(); + } + + public function onOpen(ConnectionInterface $conn, RequestInterface|null $request = null): void + { + if ($request === null) { + $conn->close(); + + return; + } + + $path = $request->getUri()->getPath(); + $this->output->writeln( + sprintf( + 'Received request for %s from %s', + $path, + $conn->remoteAddress, + ), + ); + + // Remove leading slash and any route parameters + $requestPath = trim($path, '/'); + + // For empty path (root) serve index.html + if ($requestPath === '') { + $requestPath = 'index.html'; + } + + $this->output->writeln(sprintf( + 'Request path: %s', + $requestPath, + )); + + if ($this->files->isDirectory($requestPath)) { + $requestPath .= '/index.html'; + } + + if ($this->files->has($requestPath)) { + $content = $this->files->read($requestPath) ?: ''; + + // Inject WebSocket client code for HTML files + if ($this->detector->detectMimeTypeFromPath($requestPath) === 'text/html') { + $content = $this->injectWebSocketClient($content); + } + + $headers = [ + 'Content-Type' => $this->detector->detectMimeTypeFromPath($requestPath) ?? 'text/plain', + 'Content-Length' => strlen($content), + ]; + + $conn->send(Message::toString(new Response(200, $headers, $content))); + } else { + $content = '

404 - Page Not Found

'; + $headers = [ + 'Content-Type' => 'text/html', + 'Content-Length' => strlen($content), + ]; + + $conn->send(Message::toString(new Response(404, $headers, $content))); + } + + $conn->close(); + } + + private function injectWebSocketClient(string $html): string + { + //Read html and inject script before closing body tag + $injection = <<<'EOT' + +EOT; + + return str_replace('', $injection . '', $html); + } + + function onClose(ConnectionInterface $conn): void + { + // TODO: Implement onClose() method. + } + + function onError(ConnectionInterface $conn, Throwable $e): void + { + // TODO: Implement onError() method. + } + + function onMessage(ConnectionInterface $from, $msg): void + { + // TODO: Implement onMessage() method. + } +} diff --git a/packages/guides-cli/src/Internal/UpdatePageServer.php b/packages/guides-cli/src/Internal/UpdatePageServer.php new file mode 100644 index 000000000..0fb2572bf --- /dev/null +++ b/packages/guides-cli/src/Internal/UpdatePageServer.php @@ -0,0 +1,52 @@ +clients = new SplObjectStorage(); + } + + function onOpen(ConnectionInterface $conn): void + { + $this->clients->attach($conn); + echo "New connection! ({$conn->resourceId})\n"; + } + + function onClose(ConnectionInterface $conn): void + { + // The connection is closed, remove it, as we can no longer send it messages + $this->clients->detach($conn); + + echo "Connection {$conn->resourceId} has disconnected\n"; + } + + function onError(ConnectionInterface $conn, Throwable $e): void + { + $conn->close(); + } + + public function onMessage(ConnectionInterface $conn, MessageInterface $msg): void + { + //We do nothing with the message, just a ping to keep the connection alive + } + + public function sendUpdate(): void + { + foreach ($this->clients as $client) { + $client->send('update'); + } + } +} diff --git a/packages/guides-cli/src/Watcher/INotifyWatcher.php b/packages/guides-cli/src/Watcher/INotifyWatcher.php index 294269286..4c50e5a7c 100644 --- a/packages/guides-cli/src/Watcher/INotifyWatcher.php +++ b/packages/guides-cli/src/Watcher/INotifyWatcher.php @@ -39,19 +39,31 @@ public function __invoke(): void } $path = $this->watchDescriptors[$event['wd']]['path']; - switch ($event['mask']) { - case IN_MODIFY: - $this->dispatcher->dispatch(new FileModifiedEvent($path)); - break; - case IN_CREATE: - //$this->dispatcher->dispatch(new FileCreatedEvent($path, $event['name'])); - break; - case IN_DELETE: - //$this->dispatcher->dispatch(new FileDeletedEvent($path, $event['name'])); - break; - default: - var_dump('Unhandled event mask: ' . $event['mask']); + + // File modified event - triggered by direct modification + if ($event['mask'] & IN_MODIFY) { + $this->dispatcher->dispatch(new FileModifiedEvent($path)); + return; + } + + // File closed after writing - common on macOS/Orbstack + if ($event['mask'] & IN_CLOSE_WRITE) { + $this->dispatcher->dispatch(new FileModifiedEvent($path)); + return; + } + + if ($event['mask'] & IN_CREATE) { + //$this->dispatcher->dispatch(new FileCreatedEvent($path, $event['name'])); + return; + } + + if ($event['mask'] & IN_DELETE) { + //$this->dispatcher->dispatch(new FileDeletedEvent($path, $event['name'])); + return; } + + // Log unhandled event types for debugging + var_dump('Unhandled event mask: ' . $event['mask']); } } @@ -65,7 +77,12 @@ public function addPath(string $path): void $this->loop->addReadStream($this->inotify, $this); } - $descriptor = inotify_add_watch($this->inotify, $this->inputPath . DIRECTORY_SEPARATOR . $path, IN_MODIFY | IN_CREATE | IN_DELETE); + // Add IN_CLOSE_WRITE to the watch mask to support macOS/Orbstack + $descriptor = inotify_add_watch( + $this->inotify, + $this->inputPath . DIRECTORY_SEPARATOR . $path, + IN_MODIFY | IN_CLOSE_WRITE | IN_CREATE | IN_DELETE + ); $this->watchDescriptors[$descriptor] = ['path' => $path]; } } From 6bbd075dd9f8f14a1ce554f1888f0fa690dee8e8 Mon Sep 17 00:00:00 2001 From: Jaapio Date: Mon, 25 Aug 2025 23:44:38 +0200 Subject: [PATCH 3/4] Code improvements for serve command --- composer.lock | 82 +++++++++++------ packages/guides-cli/composer.json | 2 +- .../guides-cli/resources/config/services.php | 2 + packages/guides-cli/src/Command/Serve.php | 92 +++++++------------ .../guides-cli/src/Internal/HttpHandler.php | 42 ++++----- packages/guides-cli/src/Internal/Server.php | 26 ++++++ .../guides-cli/src/Internal/ServerFactory.php | 42 +++++++++ .../Watcher/FileModifiedEvent.php | 2 +- .../{ => Internal}/Watcher/INotifyWatcher.php | 3 +- ...atePageServer.php => WebSocketHandler.php} | 8 +- 10 files changed, 185 insertions(+), 116 deletions(-) create mode 100644 packages/guides-cli/src/Internal/Server.php create mode 100644 packages/guides-cli/src/Internal/ServerFactory.php rename packages/guides-cli/src/{ => Internal}/Watcher/FileModifiedEvent.php (72%) rename packages/guides-cli/src/{ => Internal}/Watcher/INotifyWatcher.php (96%) rename packages/guides-cli/src/Internal/{UpdatePageServer.php => WebSocketHandler.php} (80%) diff --git a/composer.lock b/composer.lock index ec34e1807..9ac3b6072 100644 --- a/composer.lock +++ b/composer.lock @@ -1716,10 +1716,10 @@ "dist": { "type": "path", "url": "./packages/guides-cli", - "reference": "5eca460a69b7fb112d2b008bfeabd9c189955aee" + "reference": "783ce201a182ac9b920edc873389d9027a56f050" }, "require": { - "cboden/ratchet": "^0.4.4", + "cboden/ratchet": "0.4.x@dev", "league/mime-type-detection": "^1.16", "monolog/monolog": "^3.0", "php": "^8.1", @@ -2731,23 +2731,23 @@ }, { "name": "react/promise", - "version": "v3.2.0", + "version": "v3.3.0", "source": { "type": "git", "url": "https://github.com/reactphp/promise.git", - "reference": "8a164643313c71354582dc850b42b33fa12a4b63" + "reference": "23444f53a813a3296c1368bb104793ce8d88f04a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/reactphp/promise/zipball/8a164643313c71354582dc850b42b33fa12a4b63", - "reference": "8a164643313c71354582dc850b42b33fa12a4b63", + "url": "https://api.github.com/repos/reactphp/promise/zipball/23444f53a813a3296c1368bb104793ce8d88f04a", + "reference": "23444f53a813a3296c1368bb104793ce8d88f04a", "shasum": "" }, "require": { "php": ">=7.1.0" }, "require-dev": { - "phpstan/phpstan": "1.10.39 || 1.4.10", + "phpstan/phpstan": "1.12.28 || 1.4.10", "phpunit/phpunit": "^9.6 || ^7.5" }, "type": "library", @@ -2792,7 +2792,7 @@ ], "support": { "issues": "https://github.com/reactphp/promise/issues", - "source": "https://github.com/reactphp/promise/tree/v3.2.0" + "source": "https://github.com/reactphp/promise/tree/v3.3.0" }, "funding": [ { @@ -2800,7 +2800,7 @@ "type": "open_collective" } ], - "time": "2024-05-24T10:39:05+00:00" + "time": "2025-08-19T18:57:03+00:00" }, { "name": "react/socket", @@ -4006,7 +4006,7 @@ }, { "name": "symfony/polyfill-ctype", - "version": "v1.32.0", + "version": "v1.33.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-ctype.git", @@ -4065,7 +4065,7 @@ "portable" ], "support": { - "source": "https://github.com/symfony/polyfill-ctype/tree/v1.32.0" + "source": "https://github.com/symfony/polyfill-ctype/tree/v1.33.0" }, "funding": [ { @@ -4076,6 +4076,10 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" @@ -4085,16 +4089,16 @@ }, { "name": "symfony/polyfill-intl-grapheme", - "version": "v1.32.0", + "version": "v1.33.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-intl-grapheme.git", - "reference": "b9123926e3b7bc2f98c02ad54f6a4b02b91a8abe" + "reference": "380872130d3a5dd3ace2f4010d95125fde5d5c70" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/b9123926e3b7bc2f98c02ad54f6a4b02b91a8abe", - "reference": "b9123926e3b7bc2f98c02ad54f6a4b02b91a8abe", + "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/380872130d3a5dd3ace2f4010d95125fde5d5c70", + "reference": "380872130d3a5dd3ace2f4010d95125fde5d5c70", "shasum": "" }, "require": { @@ -4143,7 +4147,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.32.0" + "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.33.0" }, "funding": [ { @@ -4154,16 +4158,20 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2024-09-09T11:45:10+00:00" + "time": "2025-06-27T09:58:17+00:00" }, { "name": "symfony/polyfill-intl-normalizer", - "version": "v1.32.0", + "version": "v1.33.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-intl-normalizer.git", @@ -4224,7 +4232,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.32.0" + "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.33.0" }, "funding": [ { @@ -4235,6 +4243,10 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" @@ -4244,7 +4256,7 @@ }, { "name": "symfony/polyfill-mbstring", - "version": "v1.32.0", + "version": "v1.33.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-mbstring.git", @@ -4305,7 +4317,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.32.0" + "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.33.0" }, "funding": [ { @@ -4316,6 +4328,10 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" @@ -4325,7 +4341,7 @@ }, { "name": "symfony/polyfill-php80", - "version": "v1.32.0", + "version": "v1.33.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-php80.git", @@ -4385,7 +4401,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-php80/tree/v1.32.0" + "source": "https://github.com/symfony/polyfill-php80/tree/v1.33.0" }, "funding": [ { @@ -4396,6 +4412,10 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" @@ -4405,16 +4425,16 @@ }, { "name": "symfony/polyfill-php83", - "version": "v1.32.0", + "version": "v1.33.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-php83.git", - "reference": "2fb86d65e2d424369ad2905e83b236a8805ba491" + "reference": "17f6f9a6b1735c0f163024d959f700cfbc5155e5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php83/zipball/2fb86d65e2d424369ad2905e83b236a8805ba491", - "reference": "2fb86d65e2d424369ad2905e83b236a8805ba491", + "url": "https://api.github.com/repos/symfony/polyfill-php83/zipball/17f6f9a6b1735c0f163024d959f700cfbc5155e5", + "reference": "17f6f9a6b1735c0f163024d959f700cfbc5155e5", "shasum": "" }, "require": { @@ -4461,7 +4481,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-php83/tree/v1.32.0" + "source": "https://github.com/symfony/polyfill-php83/tree/v1.33.0" }, "funding": [ { @@ -4472,12 +4492,16 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2024-09-09T11:45:10+00:00" + "time": "2025-07-08T02:45:35+00:00" }, { "name": "symfony/process", diff --git a/packages/guides-cli/composer.json b/packages/guides-cli/composer.json index d08320f7a..c313f3122 100644 --- a/packages/guides-cli/composer.json +++ b/packages/guides-cli/composer.json @@ -33,7 +33,7 @@ "symfony/console": "^5.4 || ^6.3 || ^7.0", "symfony/dependency-injection": "^5.4 || ^6.3 || ^7.0", "symfony/event-dispatcher": "^5.4 || ^6.3 || ^7.0", - "cboden/ratchet": "^0.4.4", + "cboden/ratchet": "0.4.x@dev", "react/http": "^v1.11", "react/socket": "^v1.16", "league/mime-type-detection": "^1.16" diff --git a/packages/guides-cli/resources/config/services.php b/packages/guides-cli/resources/config/services.php index 4a1bb23ef..06177b49e 100644 --- a/packages/guides-cli/resources/config/services.php +++ b/packages/guides-cli/resources/config/services.php @@ -11,6 +11,7 @@ use phpDocumentor\Guides\Cli\Command\SettingsBuilder; use phpDocumentor\Guides\Cli\Internal\RunCommand; use phpDocumentor\Guides\Cli\Internal\RunCommandHandler; +use phpDocumentor\Guides\Cli\Internal\ServerFactory; use Psr\Clock\ClockInterface; use Psr\EventDispatcher\EventDispatcherInterface; use Psr\Log\LoggerInterface; @@ -56,5 +57,6 @@ ->set(SettingsBuilder::class) ->set(RunCommandHandler::class) ->tag('phpdoc.guides.command', ['command' => RunCommand::class]) + ->set(ServerFactory::class) ; }; diff --git a/packages/guides-cli/src/Command/Serve.php b/packages/guides-cli/src/Command/Serve.php index 52039d17b..e7f6401f0 100644 --- a/packages/guides-cli/src/Command/Serve.php +++ b/packages/guides-cli/src/Command/Serve.php @@ -6,39 +6,37 @@ use League\Tactician\CommandBus; use phpDocumentor\FileSystem\FlySystemAdapter; -use phpDocumentor\Guides\Cli\Internal\HttpHandler; use phpDocumentor\Guides\Cli\Internal\RunCommand; -use phpDocumentor\Guides\Cli\Internal\UpdatePageServer; -use phpDocumentor\Guides\Cli\Watcher\FileModifiedEvent; -use phpDocumentor\Guides\Cli\Watcher\INotifyWatcher; +use phpDocumentor\Guides\Cli\Internal\ServerFactory; +use phpDocumentor\Guides\Cli\Internal\Watcher\FileModifiedEvent; +use phpDocumentor\Guides\Cli\Internal\Watcher\INotifyWatcher; use phpDocumentor\Guides\Event\PostParseDocument; use Psr\EventDispatcher\EventDispatcherInterface; -use Ratchet\App; use React\EventLoop\Loop; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; -use Symfony\Component\Routing\Route; -use function function_exists; use function sprintf; final class Serve extends Command { - private UpdatePageServer $wsServer; - public function __construct( private EventDispatcherInterface $dispatcher, private SettingsBuilder $settingsBuilder, private CommandBus $commandBus, + private ServerFactory $serverFactory, ) { parent::__construct('serve'); - $this->wsServer = new UpdatePageServer(); } protected function configure(): void { $this->settingsBuilder->configureCommand($this); + $this->addOption('host', null, InputOption::VALUE_REQUIRED, 'Hostname to serve on', 'localhost'); + $this->addOption('port', 'p', InputOption::VALUE_REQUIRED, 'Port to run the server on', 1337); + $this->addOption('listen', 'l', InputOption::VALUE_REQUIRED, 'Address to listen on', '0.0.0.0'); } protected function execute(InputInterface $input, OutputInterface $output): int @@ -47,6 +45,29 @@ protected function execute(InputInterface $input, OutputInterface $output): int declare(ticks=1); $loop = Loop::get(); + $loop->addSignal(SIGINT, static function () use ($loop, $output): void { + $output->writeln('Shutting down server...'); + $loop->stop(); + $output->writeln('Server stopped'); + exit(0); + }); + + + $dir = $input->getOption('output'); + if ($dir === null) { + $output->writeln('Please specify an output directory using --output option'); + + return Command::FAILURE; + } + + $files = FlySystemAdapter::createForPath($dir); + $app = $this->serverFactory->createWebserver( + $files, + $loop, + $input->getOption('host'), + $input->getOption('listen'), + (int) $input->getOption('port'), + ); $watcher = new InotifyWatcher($loop, $this->dispatcher, $input->getArgument('input')); @@ -59,7 +80,7 @@ static function (PostParseDocument $event) use ($watcher): void { $this->dispatcher->addListener( FileModifiedEvent::class, - function (FileModifiedEvent $event) use ($input, $output): void { + function (FileModifiedEvent $event) use ($app, $input, $output): void { $output->writeln( sprintf( 'File modified: %s, rerendering...', @@ -74,9 +95,7 @@ function (FileModifiedEvent $event) use ($input, $output): void { ), ); $output->writeln('Rerendering completed.'); - - // Notify connected clients that they should reload - $this->wsServer->sendUpdate(); + $app->notifyClients(); }, ); @@ -90,55 +109,10 @@ function (FileModifiedEvent $event) use ($input, $output): void { ), ); - $dir = $input->getOption('output'); - if ($dir === null) { - $output->writeln('Please specify an output directory using --output option'); - - return Command::FAILURE; - } - - $files = FlySystemAdapter::createForPath($dir); - - // Create HTTP handler for serving files - $httpHandler = new HttpHandler($output, $files); - - // Setup the Ratchet App with routes - $app = new App('localhost', 1337, '0.0.0.0', $loop); - - // Add WebSocket route at /ws - $app->route('/ws', $this->wsServer, ['*']); - - // Add root path first - $app->route('/', $httpHandler, ['*']); - - // Add HTTP server for all other routes - use a different pattern syntax - $app->routes->add('catch-all', new Route( - '/{url}', - ['_controller' => $httpHandler], - ['url' => '.+'], - [], - 'localhost', - [], - ['GET'], - )); - $output->writeln(sprintf('Server running at http://localhost:1337')); $output->writeln('WebSocket server running at ws://localhost:1337/ws'); $output->writeln('Press Ctrl+C to stop the server'); - // Handle SIGINT (Ctrl+C) gracefully if PCNTL extension is available - if (function_exists('pcntl_signal')) { - // 2 is the signal number for SIGINT (Ctrl+C) - pcntl_signal(2, static function () use ($loop, $output): void { - $output->writeln('Shutting down server...'); - $loop->stop(); - $output->writeln('Server stopped'); - exit(0); - }); - } else { - $output->writeln('Note: PCNTL extension not available, Ctrl+C handling may not work properly'); - } - $app->run(); return Command::SUCCESS; diff --git a/packages/guides-cli/src/Internal/HttpHandler.php b/packages/guides-cli/src/Internal/HttpHandler.php index 8968d2d5c..fe3ef1340 100644 --- a/packages/guides-cli/src/Internal/HttpHandler.php +++ b/packages/guides-cli/src/Internal/HttpHandler.php @@ -9,9 +9,10 @@ use League\MimeTypeDetection\ExtensionMimeTypeDetector; use phpDocumentor\FileSystem\FlySystemAdapter; use Psr\Http\Message\RequestInterface; +use Psr\Log\LoggerInterface; use Ratchet\ConnectionInterface; +use Ratchet\Http\CloseResponseTrait; use Ratchet\Http\HttpServerInterface; -use Symfony\Component\Console\Output\OutputInterface; use Throwable; use function sprintf; @@ -21,10 +22,12 @@ final class HttpHandler implements HttpServerInterface { + use CloseResponseTrait; + private ExtensionMimeTypeDetector $detector; public function __construct( - private OutputInterface $output, + private LoggerInterface $logger, private FlySystemAdapter $files, ) { $this->detector = new ExtensionMimeTypeDetector(); @@ -39,7 +42,7 @@ public function onOpen(ConnectionInterface $conn, RequestInterface|null $request } $path = $request->getUri()->getPath(); - $this->output->writeln( + $this->logger->info( sprintf( 'Received request for %s from %s', $path, @@ -55,11 +58,6 @@ public function onOpen(ConnectionInterface $conn, RequestInterface|null $request $requestPath = 'index.html'; } - $this->output->writeln(sprintf( - 'Request path: %s', - $requestPath, - )); - if ($this->files->isDirectory($requestPath)) { $requestPath .= '/index.html'; } @@ -78,16 +76,17 @@ public function onOpen(ConnectionInterface $conn, RequestInterface|null $request ]; $conn->send(Message::toString(new Response(200, $headers, $content))); - } else { - $content = '

404 - Page Not Found

'; - $headers = [ - 'Content-Type' => 'text/html', - 'Content-Length' => strlen($content), - ]; - - $conn->send(Message::toString(new Response(404, $headers, $content))); + $conn->close(); + return; } + $content = '

404 - Page Not Found

'; + $headers = [ + 'Content-Type' => 'text/html', + 'Content-Length' => strlen($content), + ]; + + $conn->send(Message::toString(new Response(404, $headers, $content))); $conn->close(); } @@ -109,17 +108,18 @@ private function injectWebSocketClient(string $html): string return str_replace('', $injection . '', $html); } - function onClose(ConnectionInterface $conn): void + public function onClose(ConnectionInterface $conn): void { - // TODO: Implement onClose() method. + $this->close($conn); } - function onError(ConnectionInterface $conn, Throwable $e): void + public function onError(ConnectionInterface $conn, Throwable $e): void { - // TODO: Implement onError() method. + $this->close($conn, 500); } - function onMessage(ConnectionInterface $from, $msg): void + /** @param string $msg */ + public function onMessage(ConnectionInterface $from, $msg): void { // TODO: Implement onMessage() method. } diff --git a/packages/guides-cli/src/Internal/Server.php b/packages/guides-cli/src/Internal/Server.php new file mode 100644 index 000000000..6763a8575 --- /dev/null +++ b/packages/guides-cli/src/Internal/Server.php @@ -0,0 +1,26 @@ +app->run(); + } + + public function notifyClients(): void + { + $this->webSocketHandler->sendUpdate(); + } +} diff --git a/packages/guides-cli/src/Internal/ServerFactory.php b/packages/guides-cli/src/Internal/ServerFactory.php new file mode 100644 index 000000000..efb9a1ba6 --- /dev/null +++ b/packages/guides-cli/src/Internal/ServerFactory.php @@ -0,0 +1,42 @@ +logger, $files); + $wsServer = new WebSocketHandler(); + $host = 'localhost'; + + + // Setup the Ratchet App with routes + $app = new App($host, $port, $listen, $loop); + + // Add WebSocket route at /ws + $app->route('/ws', $wsServer, ['*']); + + // Add HTTP server for all other routes - use a different pattern syntax + $app->routes->add('catch-all', new Route( + '/{url}', + ['_controller' => $httpHandler], + ['url' => '.*'], + methods: ['GET'], + )); + + return new Server($app, $wsServer); + } +} diff --git a/packages/guides-cli/src/Watcher/FileModifiedEvent.php b/packages/guides-cli/src/Internal/Watcher/FileModifiedEvent.php similarity index 72% rename from packages/guides-cli/src/Watcher/FileModifiedEvent.php rename to packages/guides-cli/src/Internal/Watcher/FileModifiedEvent.php index 7e402ba90..da5ad6bdd 100644 --- a/packages/guides-cli/src/Watcher/FileModifiedEvent.php +++ b/packages/guides-cli/src/Internal/Watcher/FileModifiedEvent.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace phpDocumentor\Guides\Cli\Watcher; +namespace phpDocumentor\Guides\Cli\Internal\Watcher; final class FileModifiedEvent { diff --git a/packages/guides-cli/src/Watcher/INotifyWatcher.php b/packages/guides-cli/src/Internal/Watcher/INotifyWatcher.php similarity index 96% rename from packages/guides-cli/src/Watcher/INotifyWatcher.php rename to packages/guides-cli/src/Internal/Watcher/INotifyWatcher.php index 4c50e5a7c..db5710682 100644 --- a/packages/guides-cli/src/Watcher/INotifyWatcher.php +++ b/packages/guides-cli/src/Internal/Watcher/INotifyWatcher.php @@ -2,8 +2,9 @@ declare(strict_types=1); -namespace phpDocumentor\Guides\Cli\Watcher; +namespace phpDocumentor\Guides\Cli\Internal\Watcher; +use Evenement\EventEmitter; use Psr\EventDispatcher\EventDispatcherInterface; use React\EventLoop\LoopInterface; diff --git a/packages/guides-cli/src/Internal/UpdatePageServer.php b/packages/guides-cli/src/Internal/WebSocketHandler.php similarity index 80% rename from packages/guides-cli/src/Internal/UpdatePageServer.php rename to packages/guides-cli/src/Internal/WebSocketHandler.php index 0fb2572bf..6a38e5578 100644 --- a/packages/guides-cli/src/Internal/UpdatePageServer.php +++ b/packages/guides-cli/src/Internal/WebSocketHandler.php @@ -10,7 +10,7 @@ use SplObjectStorage; use Throwable; -final class UpdatePageServer implements MessageComponentInterface +final class WebSocketHandler implements MessageComponentInterface { private $clients; @@ -19,13 +19,13 @@ public function __construct() $this->clients = new SplObjectStorage(); } - function onOpen(ConnectionInterface $conn): void + public function onOpen(ConnectionInterface $conn): void { $this->clients->attach($conn); echo "New connection! ({$conn->resourceId})\n"; } - function onClose(ConnectionInterface $conn): void + public function onClose(ConnectionInterface $conn): void { // The connection is closed, remove it, as we can no longer send it messages $this->clients->detach($conn); @@ -33,7 +33,7 @@ function onClose(ConnectionInterface $conn): void echo "Connection {$conn->resourceId} has disconnected\n"; } - function onError(ConnectionInterface $conn, Throwable $e): void + public function onError(ConnectionInterface $conn, Throwable $e): void { $conn->close(); } From 5d4d9be50de31a779b7aa270fea4a94333b7da18 Mon Sep 17 00:00:00 2001 From: Jaapio Date: Mon, 25 Aug 2025 23:59:35 +0200 Subject: [PATCH 4/4] Fix code style --- .../guides-cli/resources/config/services.php | 5 ++-- packages/guides-cli/src/Command/Run.php | 25 ------------------- packages/guides-cli/src/Command/Serve.php | 13 ++++++++++ .../src/Command/SettingsBuilder.php | 10 +++++++- .../guides-cli/src/Internal/HttpHandler.php | 13 ++++++++-- .../guides-cli/src/Internal/RunCommand.php | 12 +++++++-- .../src/Internal/RunCommandHandler.php | 20 +++++++++++++-- packages/guides-cli/src/Internal/Server.php | 9 +++++++ .../guides-cli/src/Internal/ServerFactory.php | 11 +++++++- .../Internal/Watcher/FileModifiedEvent.php | 9 +++++++ .../src/Internal/Watcher/INotifyWatcher.php | 19 +++++++++++--- .../src/Internal/WebSocketHandler.php | 21 +++++++++++----- 12 files changed, 122 insertions(+), 45 deletions(-) diff --git a/packages/guides-cli/resources/config/services.php b/packages/guides-cli/resources/config/services.php index 06177b49e..d4a7ba16a 100644 --- a/packages/guides-cli/resources/config/services.php +++ b/packages/guides-cli/resources/config/services.php @@ -7,8 +7,8 @@ use phpDocumentor\Guides\Cli\Command\ProgressBarSubscriber; use phpDocumentor\Guides\Cli\Command\Run; use phpDocumentor\Guides\Cli\Command\Serve; -use phpDocumentor\Guides\Cli\Command\WorkingDirectorySwitcher; use phpDocumentor\Guides\Cli\Command\SettingsBuilder; +use phpDocumentor\Guides\Cli\Command\WorkingDirectorySwitcher; use phpDocumentor\Guides\Cli\Internal\RunCommand; use phpDocumentor\Guides\Cli\Internal\RunCommandHandler; use phpDocumentor\Guides\Cli\Internal\ServerFactory; @@ -57,6 +57,5 @@ ->set(SettingsBuilder::class) ->set(RunCommandHandler::class) ->tag('phpdoc.guides.command', ['command' => RunCommand::class]) - ->set(ServerFactory::class) - ; + ->set(ServerFactory::class); }; diff --git a/packages/guides-cli/src/Command/Run.php b/packages/guides-cli/src/Command/Run.php index aee6b20fe..f5c7d1968 100644 --- a/packages/guides-cli/src/Command/Run.php +++ b/packages/guides-cli/src/Command/Run.php @@ -14,52 +14,27 @@ namespace phpDocumentor\Guides\Cli\Command; use Doctrine\Deprecations\Deprecation; -use Flyfinder\Path; -use Flyfinder\Specification\InPath; -use Flyfinder\Specification\NotSpecification; -use Flyfinder\Specification\OrSpecification; -use Flyfinder\Specification\SpecificationInterface; use League\Tactician\CommandBus; use Monolog\Handler\ErrorLogHandler; use Monolog\Handler\StreamHandler; use Monolog\Logger; -use phpDocumentor\FileSystem\Finder\Exclude; -use phpDocumentor\FileSystem\FlySystemAdapter; use phpDocumentor\Guides\Cli\Internal\RunCommand; use phpDocumentor\Guides\Cli\Logger\SpyProcessor; -use phpDocumentor\Guides\Compiler\CompilerContext; -use phpDocumentor\Guides\Event\PostProjectNodeCreated; -use phpDocumentor\Guides\Handlers\CompileDocumentsCommand; -use phpDocumentor\Guides\Handlers\ParseDirectoryCommand; -use phpDocumentor\Guides\Handlers\ParseFileCommand; -use phpDocumentor\Guides\Handlers\RenderCommand; -use phpDocumentor\Guides\Nodes\ProjectNode; -use phpDocumentor\Guides\Settings\ProjectSettings; use phpDocumentor\Guides\Settings\SettingsManager; use phpDocumentor\Guides\Twig\Theme\ThemeManager; use Psr\Clock\ClockInterface; use Psr\Log\LogLevel; -use RuntimeException; use Symfony\Component\Console\Command\Command; -use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\ConsoleOutputInterface; use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\EventDispatcher\EventDispatcher; -use function array_map; use function array_pop; -use function array_reduce; -use function array_shift; -use function assert; use function count; use function implode; use function is_countable; -use function is_dir; -use function method_exists; -use function pathinfo; -use function sprintf; use function strtoupper; final class Run extends Command diff --git a/packages/guides-cli/src/Command/Serve.php b/packages/guides-cli/src/Command/Serve.php index e7f6401f0..239bfe141 100644 --- a/packages/guides-cli/src/Command/Serve.php +++ b/packages/guides-cli/src/Command/Serve.php @@ -2,9 +2,19 @@ declare(strict_types=1); +/** + * This file is part of phpDocumentor. + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + * + * @link https://phpdoc.org + */ + namespace phpDocumentor\Guides\Cli\Command; use League\Tactician\CommandBus; +use Monolog\Handler\ErrorLogHandler; use phpDocumentor\FileSystem\FlySystemAdapter; use phpDocumentor\Guides\Cli\Internal\RunCommand; use phpDocumentor\Guides\Cli\Internal\ServerFactory; @@ -12,6 +22,7 @@ use phpDocumentor\Guides\Cli\Internal\Watcher\INotifyWatcher; use phpDocumentor\Guides\Event\PostParseDocument; use Psr\EventDispatcher\EventDispatcherInterface; +use Psr\Log\LoggerInterface; use React\EventLoop\Loop; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputInterface; @@ -23,6 +34,7 @@ final class Serve extends Command { public function __construct( + private LoggerInterface $logger, private EventDispatcherInterface $dispatcher, private SettingsBuilder $settingsBuilder, private CommandBus $commandBus, @@ -41,6 +53,7 @@ protected function configure(): void protected function execute(InputInterface $input, OutputInterface $output): int { + $this->logger->pushHandler(new ErrorLogHandler(ErrorLogHandler::OPERATING_SYSTEM)); // Enable tick processing for signal handling declare(ticks=1); diff --git a/packages/guides-cli/src/Command/SettingsBuilder.php b/packages/guides-cli/src/Command/SettingsBuilder.php index e44771647..448105d36 100644 --- a/packages/guides-cli/src/Command/SettingsBuilder.php +++ b/packages/guides-cli/src/Command/SettingsBuilder.php @@ -2,6 +2,15 @@ declare(strict_types=1); +/** + * This file is part of phpDocumentor. + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + * + * @link https://phpdoc.org + */ + namespace phpDocumentor\Guides\Cli\Command; use phpDocumentor\Guides\Event\PostProjectNodeCreated; @@ -175,6 +184,5 @@ public function configureCommand(Command $command): void InputOption::VALUE_REQUIRED, 'The theme used for rendering', ); - } } diff --git a/packages/guides-cli/src/Internal/HttpHandler.php b/packages/guides-cli/src/Internal/HttpHandler.php index fe3ef1340..090247f59 100644 --- a/packages/guides-cli/src/Internal/HttpHandler.php +++ b/packages/guides-cli/src/Internal/HttpHandler.php @@ -2,6 +2,15 @@ declare(strict_types=1); +/** + * This file is part of phpDocumentor. + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + * + * @link https://phpdoc.org + */ + namespace phpDocumentor\Guides\Cli\Internal; use GuzzleHttp\Psr7\Message; @@ -77,6 +86,7 @@ public function onOpen(ConnectionInterface $conn, RequestInterface|null $request $conn->send(Message::toString(new Response(200, $headers, $content))); $conn->close(); + return; } @@ -118,8 +128,7 @@ public function onError(ConnectionInterface $conn, Throwable $e): void $this->close($conn, 500); } - /** @param string $msg */ - public function onMessage(ConnectionInterface $from, $msg): void + public function onMessage(ConnectionInterface $from, string $msg): void { // TODO: Implement onMessage() method. } diff --git a/packages/guides-cli/src/Internal/RunCommand.php b/packages/guides-cli/src/Internal/RunCommand.php index 9ffa63fb9..21ae6e6bb 100644 --- a/packages/guides-cli/src/Internal/RunCommand.php +++ b/packages/guides-cli/src/Internal/RunCommand.php @@ -2,6 +2,15 @@ declare(strict_types=1); +/** + * This file is part of phpDocumentor. + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + * + * @link https://phpdoc.org + */ + namespace phpDocumentor\Guides\Cli\Internal; use phpDocumentor\Guides\Nodes\ProjectNode; @@ -14,7 +23,6 @@ public function __construct( public readonly ProjectSettings $settings, public readonly ProjectNode $projectNode, public readonly InputInterface|null $input, - ) - { + ) { } } diff --git a/packages/guides-cli/src/Internal/RunCommandHandler.php b/packages/guides-cli/src/Internal/RunCommandHandler.php index 7a2b0454c..6ad07ae48 100644 --- a/packages/guides-cli/src/Internal/RunCommandHandler.php +++ b/packages/guides-cli/src/Internal/RunCommandHandler.php @@ -2,6 +2,15 @@ declare(strict_types=1); +/** + * This file is part of phpDocumentor. + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + * + * @link https://phpdoc.org + */ + namespace phpDocumentor\Guides\Cli\Internal; use Flyfinder\Path; @@ -17,19 +26,26 @@ use phpDocumentor\Guides\Handlers\ParseDirectoryCommand; use phpDocumentor\Guides\Handlers\ParseFileCommand; use phpDocumentor\Guides\Handlers\RenderCommand; +use phpDocumentor\Guides\Nodes\DocumentNode; use phpDocumentor\Guides\Settings\ProjectSettings; use phpDocumentor\Guides\Twig\Theme\ThemeManager; use Symfony\Component\Console\Input\InputInterface; +use function array_map; +use function array_reduce; +use function array_shift; +use function assert; +use function method_exists; + class RunCommandHandler { public function __construct( private CommandBus $commandBus, private ThemeManager $themeManager, - ) - { + ) { } + /** @return DocumentNode[] */ public function handle(RunCommand $command): array { $settings = $command->settings; diff --git a/packages/guides-cli/src/Internal/Server.php b/packages/guides-cli/src/Internal/Server.php index 6763a8575..001a1f9a9 100644 --- a/packages/guides-cli/src/Internal/Server.php +++ b/packages/guides-cli/src/Internal/Server.php @@ -2,6 +2,15 @@ declare(strict_types=1); +/** + * This file is part of phpDocumentor. + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + * + * @link https://phpdoc.org + */ + namespace phpDocumentor\Guides\Cli\Internal; use Ratchet\App; diff --git a/packages/guides-cli/src/Internal/ServerFactory.php b/packages/guides-cli/src/Internal/ServerFactory.php index efb9a1ba6..9fc81f2f1 100644 --- a/packages/guides-cli/src/Internal/ServerFactory.php +++ b/packages/guides-cli/src/Internal/ServerFactory.php @@ -2,6 +2,15 @@ declare(strict_types=1); +/** + * This file is part of phpDocumentor. + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + * + * @link https://phpdoc.org + */ + namespace phpDocumentor\Guides\Cli\Internal; use phpDocumentor\FileSystem\FlySystemAdapter; @@ -19,7 +28,7 @@ public function __construct(private LoggerInterface $logger) public function createWebserver(FlySystemAdapter $files, LoopInterface|null $loop, string $host, string $listen, int $port): Server { $httpHandler = new HttpHandler($this->logger, $files); - $wsServer = new WebSocketHandler(); + $wsServer = new WebSocketHandler($this->logger); $host = 'localhost'; diff --git a/packages/guides-cli/src/Internal/Watcher/FileModifiedEvent.php b/packages/guides-cli/src/Internal/Watcher/FileModifiedEvent.php index da5ad6bdd..17861b768 100644 --- a/packages/guides-cli/src/Internal/Watcher/FileModifiedEvent.php +++ b/packages/guides-cli/src/Internal/Watcher/FileModifiedEvent.php @@ -2,6 +2,15 @@ declare(strict_types=1); +/** + * This file is part of phpDocumentor. + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + * + * @link https://phpdoc.org + */ + namespace phpDocumentor\Guides\Cli\Internal\Watcher; final class FileModifiedEvent diff --git a/packages/guides-cli/src/Internal/Watcher/INotifyWatcher.php b/packages/guides-cli/src/Internal/Watcher/INotifyWatcher.php index db5710682..8b3cbfe66 100644 --- a/packages/guides-cli/src/Internal/Watcher/INotifyWatcher.php +++ b/packages/guides-cli/src/Internal/Watcher/INotifyWatcher.php @@ -2,9 +2,17 @@ declare(strict_types=1); +/** + * This file is part of phpDocumentor. + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + * + * @link https://phpdoc.org + */ + namespace phpDocumentor\Guides\Cli\Internal\Watcher; -use Evenement\EventEmitter; use Psr\EventDispatcher\EventDispatcherInterface; use React\EventLoop\LoopInterface; @@ -13,6 +21,8 @@ use function stream_set_blocking; use function var_dump; +use const DIRECTORY_SEPARATOR; + class INotifyWatcher { /** @var resource|false */ @@ -30,7 +40,8 @@ public function __construct( public function __invoke(): void { - if (($events = inotify_read($this->inotify)) === false) { + $events = inotify_read($this->inotify); + if ($events === false) { return; } @@ -44,12 +55,14 @@ public function __invoke(): void // File modified event - triggered by direct modification if ($event['mask'] & IN_MODIFY) { $this->dispatcher->dispatch(new FileModifiedEvent($path)); + return; } // File closed after writing - common on macOS/Orbstack if ($event['mask'] & IN_CLOSE_WRITE) { $this->dispatcher->dispatch(new FileModifiedEvent($path)); + return; } @@ -82,7 +95,7 @@ public function addPath(string $path): void $descriptor = inotify_add_watch( $this->inotify, $this->inputPath . DIRECTORY_SEPARATOR . $path, - IN_MODIFY | IN_CLOSE_WRITE | IN_CREATE | IN_DELETE + IN_MODIFY | IN_CLOSE_WRITE | IN_CREATE | IN_DELETE, ); $this->watchDescriptors[$descriptor] = ['path' => $path]; } diff --git a/packages/guides-cli/src/Internal/WebSocketHandler.php b/packages/guides-cli/src/Internal/WebSocketHandler.php index 6a38e5578..3b3c6fa8e 100644 --- a/packages/guides-cli/src/Internal/WebSocketHandler.php +++ b/packages/guides-cli/src/Internal/WebSocketHandler.php @@ -2,8 +2,18 @@ declare(strict_types=1); +/** + * This file is part of phpDocumentor. + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + * + * @link https://phpdoc.org + */ + namespace phpDocumentor\Guides\Cli\Internal; +use Psr\Log\LoggerInterface; use Ratchet\ConnectionInterface; use Ratchet\RFC6455\Messaging\MessageInterface; use Ratchet\WebSocket\MessageComponentInterface; @@ -12,9 +22,10 @@ final class WebSocketHandler implements MessageComponentInterface { - private $clients; + /** @var SplObjectStorage */ + private SplObjectStorage $clients; - public function __construct() + public function __construct(private LoggerInterface $logger) { $this->clients = new SplObjectStorage(); } @@ -22,15 +33,13 @@ public function __construct() public function onOpen(ConnectionInterface $conn): void { $this->clients->attach($conn); - echo "New connection! ({$conn->resourceId})\n"; + $this->logger->info('New WebSocket connection {resourceId}', ['resourceId' => $conn->resourceId]); } public function onClose(ConnectionInterface $conn): void { - // The connection is closed, remove it, as we can no longer send it messages $this->clients->detach($conn); - - echo "Connection {$conn->resourceId} has disconnected\n"; + $this->logger->info('WebSocket connection {resourceId} has disconnected', ['resourceId' => $conn->resourceId]); } public function onError(ConnectionInterface $conn, Throwable $e): void