From c6a4f106aadfe0b907174453879457365b7436a0 Mon Sep 17 00:00:00 2001 From: "seregazhuk88@gmail.com" Date: Wed, 20 May 2020 20:31:53 +0300 Subject: [PATCH 01/59] Use different fs watchers --- .travis.yml | 16 ---- CHANGELOG.md | 4 +- README.md | 9 ++ composer.json | 5 +- phpunit.xml.dist | 3 - src/Filesystem/ChangesListener.php | 7 +- src/Filesystem/Factory.php | 22 +++++ .../FsWatchBased/ChangesListener.php | 89 +++++++++++++++++++ .../ResourceWatcherBased/ChangesListener.php | 28 +++--- src/Watcher.php | 10 +-- src/WatcherCommand.php | 20 +++-- tests/Feature/ChangesListenerTest.php | 4 +- tests/Feature/Helper/Filesystem.php | 2 +- tests/Feature/Helper/WatcherTestCase.php | 5 +- tests/Feature/IgnoreFilesTest.php | 1 - 15 files changed, 168 insertions(+), 57 deletions(-) create mode 100644 src/Filesystem/Factory.php create mode 100644 src/Filesystem/FsWatchBased/ChangesListener.php diff --git a/.travis.yml b/.travis.yml index 12a2442..dae8b72 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,22 +2,6 @@ language: php jobs: include: - - stage: "PHP7.2 - lowest" - php: 7.2 - script: - - composer update -n --prefer-dist --prefer-lowest --no-suggest - - composer dump-autoload - - composer ci:tests - - composer ci:php:psalm - - - stage: "PHP7.3 - highest" - php: 7.3 - script: - - composer update -n --prefer-dist --no-suggest - - composer dump-autoload - - composer ci:tests - - composer ci:php:psalm - - stage: "PHP7.4 - highest" php: 7.4 script: diff --git a/CHANGELOG.md b/CHANGELOG.md index 73c77d6..cf9a5e0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,7 @@ -## 0.6.0 (2020-05-11) +## 0.6.0 (2020-05-20) * Fix: don't use child process for resource watching +* Feature: add fswatch support +* Fix: min required PHP version is set to 7.4 ## 0.5.2 (2019-12-07) * Fix: use predefined const for PHP binary [#59](https://github.com/seregazhuk/php-watcher/pull/59) diff --git a/README.md b/README.md index 7538d8e..bd9c30b 100644 --- a/README.md +++ b/README.md @@ -37,6 +37,7 @@ PHP-watcher does not require any additional changes to your code or method of * [Default executable](#default-executable) * [Gracefully reloading down your script](#gracefully-reloading-down-your-script) * [Automatic restart](#automatic-restart) +* [Performance](#performance) * [Spinner](#spinner) ## Installation @@ -235,6 +236,14 @@ script crashes PHP-watcher will notify you about that. ![app exit](images/exit.svg) +## Performance + +The watcher can use different strategies to monitor your file system changes. Under the hood it +detects the environment and chooses the best suitable strategy. + +By default, it uses [yosymfony/resource-watcher](https://github.com/yosymfony/resource-watcher +) which + ## Spinner By default the watcher outputs a nice spinner which indicates that the process is running diff --git a/composer.json b/composer.json index 9bed73c..e3e63d2 100644 --- a/composer.json +++ b/composer.json @@ -24,7 +24,7 @@ } ], "require": { - "php": "^7.2", + "php": "^7.4", "ext-json": "*", "ext-pcntl": "*", "yosymfony/resource-watcher": "^2.0", @@ -34,7 +34,8 @@ "react/child-process": "^0.6.1", "react/stream": "^1.0.0", "symfony/finder": "^4.3 || ^5.0", - "alecrabbit/php-cli-snake": "^0.5" + "alecrabbit/php-cli-snake": "^0.5", + "seregazhuk/reactphp-fswatch": "^0.1.0" }, "autoload": { "psr-4": { diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 4c99dc1..23319e3 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -16,9 +16,6 @@ src - - src/Filesystem/watcher.php - diff --git a/src/Filesystem/ChangesListener.php b/src/Filesystem/ChangesListener.php index 5acf8d7..00c0f74 100644 --- a/src/Filesystem/ChangesListener.php +++ b/src/Filesystem/ChangesListener.php @@ -1,13 +1,14 @@ fsWatch = new FsWatch($this->makeOptions($watchList), $loop); + } + + public static function isAvailable(): bool + { + return FsWatch::isAvailable(); + } + + public function start(): void + { + $this->fsWatch->run(); + $this->fsWatch->on( + 'change', + function () { + $this->emit('change'); + } + ); + } + + public function onChange(callable $callback): void + { + $this->on('change', $callback); + } + + public function stop(): void + { + $this->fsWatch->stop(); + } + + private function makeOptions(WatchList $watchList): string + { + $options = []; + + // first come paths + if ($watchList->paths()) { + $options[] = implode(' ', $watchList->paths()); + } + + // then we ignore + if ($watchList->ignore()) { + $options[] = '-e ' . implode(' ', $watchList->ignore()); + } + + // then include + if ($watchList->fileExtensions()) { + $options = array_merge($options, $this->makeIncludeOptions($watchList)); + } + + $options[] = '-I'; // Case-insensitive + + return implode(' ', $options); + } + + private function makeIncludeOptions(WatchList $watchList): array + { + $options = []; + // Before including we need to ignore everything + if (empty($watchList->ignore())) { + $options[] = '-e ".*"'; + } + + $regexpWithExtensions = array_map( + static function ($extension) { + return str_replace('*.', '.', $extension) . '$'; + }, + $watchList->fileExtensions() + ); + $options[] = '-i ' . implode(' ', $regexpWithExtensions); + return $options; + } +} diff --git a/src/Filesystem/ResourceWatcherBased/ChangesListener.php b/src/Filesystem/ResourceWatcherBased/ChangesListener.php index 7fe5955..2cb80d2 100644 --- a/src/Filesystem/ResourceWatcherBased/ChangesListener.php +++ b/src/Filesystem/ResourceWatcherBased/ChangesListener.php @@ -1,31 +1,35 @@ -loop = $loop; + $this->watcher = ResourceWatcherBuilder::create($watchList); } - public function start(WatchList $watchList): void + public function start(): void { - $watcher = ResourceWatcherBuilder::create($watchList); - $this->loop->addPeriodicTimer( self::INTERVAL, - function () use ($watcher) { - if ($watcher->findChanges()->hasChanges()) { + function () { + if ($this->watcher->findChanges()->hasChanges()) { $this->emit('change'); } } @@ -36,4 +40,8 @@ public function onChange(callable $callback): void { $this->on('change', $callback); } + + public function stop(): void + { + } } diff --git a/src/Watcher.php b/src/Watcher.php index 76c138f..b447a7a 100644 --- a/src/Watcher.php +++ b/src/Watcher.php @@ -5,14 +5,13 @@ namespace seregazhuk\PhpWatcher; use React\EventLoop\LoopInterface; -use seregazhuk\PhpWatcher\Config\WatchList; -use seregazhuk\PhpWatcher\Filesystem\ResourceWatcherBased\ChangesListener; +use seregazhuk\PhpWatcher\Filesystem\ChangesListener; final class Watcher { - private $loop; + private LoopInterface $loop; - private $filesystemListener; + private ChangesListener $filesystemListener; public function __construct(LoopInterface $loop, ChangesListener $filesystemListener) { @@ -22,13 +21,12 @@ public function __construct(LoopInterface $loop, ChangesListener $filesystemList public function startWatching( ProcessRunner $processRunner, - WatchList $watchList, int $signal, float $delayToRestart ): void { $processRunner->start(); - $this->filesystemListener->start($watchList); + $this->filesystemListener->start(); $this->filesystemListener->onChange( static function () use ($processRunner, $signal, $delayToRestart) { $processRunner->stop($signal); diff --git a/src/WatcherCommand.php b/src/WatcherCommand.php index bcb65a6..58ac603 100644 --- a/src/WatcherCommand.php +++ b/src/WatcherCommand.php @@ -1,4 +1,5 @@ buildConfig(new InputExtractor($input)); $spinner = SpinnerFactory::create($output, $config->spinnerDisabled()); - $this->addTerminationListeners($loop, $spinner); - $screen = new Screen(new SymfonyStyle($input, $output), $spinner); - $filesystem = new ChangesListener($loop); + $filesystem = ChangesListenerFactory::create($config->watchList(), $loop); $screen->showOptions($config->watchList()); $processRunner = new ProcessRunner($loop, $screen, $config->command()); + $this->addTerminationListeners($loop, $spinner, $filesystem, $processRunner); $watcher = new Watcher($loop, $filesystem); $watcher->startWatching( $processRunner, - $config->watchList(), $config->signalToReload(), $config->delay() ); @@ -82,10 +82,14 @@ protected function execute(InputInterface $input, OutputInterface $output) /** * When terminating the watcher we need to manually restore the cursor after the spinner. */ - private function addTerminationListeners(LoopInterface $loop, SpinnerInterface $spinner): void - { - $func = static function (int $signal) use ($spinner): void { + private function addTerminationListeners( + LoopInterface $loop, + SpinnerInterface $spinner, + ChangesListener $changesListener + ): void { + $func = static function (int $signal) use ($spinner, $changesListener): void { $spinner->end(); + $changesListener->stop(); exit($signal); }; diff --git a/tests/Feature/ChangesListenerTest.php b/tests/Feature/ChangesListenerTest.php index 0753eb4..30a30d4 100644 --- a/tests/Feature/ChangesListenerTest.php +++ b/tests/Feature/ChangesListenerTest.php @@ -19,8 +19,8 @@ final class ChangesListenerTest extends TestCase public function it_emits_change_event_on_changes(): void { $loop = Factory::create(); - $listener = new ChangesListener($loop); - $listener->start(new WatchList([Filesystem::fixturesDir()])); + $listener = new ChangesListener(new WatchList([Filesystem::fixturesDir()]), $loop); + $listener->start(); $loop->addTimer(1, [Filesystem::class, 'createHelloWorldPHPFile']); $eventWasEmitted = false; diff --git a/tests/Feature/Helper/Filesystem.php b/tests/Feature/Helper/Filesystem.php index ef2f974..e1af2f1 100644 --- a/tests/Feature/Helper/Filesystem.php +++ b/tests/Feature/Helper/Filesystem.php @@ -75,7 +75,7 @@ public static function changeFileContentsWith(string $file, string $contents): v public static function fixturesDir(): string { - return str_replace('tests/', '', self::FIXTURES_DIR); + return self::FIXTURES_DIR; } private static function buildFilePath(string $filename): string diff --git a/tests/Feature/Helper/WatcherTestCase.php b/tests/Feature/Helper/WatcherTestCase.php index a9cd715..0ee5b73 100644 --- a/tests/Feature/Helper/WatcherTestCase.php +++ b/tests/Feature/Helper/WatcherTestCase.php @@ -11,10 +11,7 @@ abstract class WatcherTestCase extends TestCase private const WAIT_TIMEOUT_MS = 5000000; - /** - * @var Process - */ - private $watcherRunner; + private Process $watcherRunner; protected function wait(): void { diff --git a/tests/Feature/IgnoreFilesTest.php b/tests/Feature/IgnoreFilesTest.php index 2ccd526..9cc6df0 100644 --- a/tests/Feature/IgnoreFilesTest.php +++ b/tests/Feature/IgnoreFilesTest.php @@ -7,7 +7,6 @@ final class IgnoreFilesTest extends WatcherTestCase { - /** @test */ public function it_doesnt_reload_when_ignored_files_change(): void { $fileToWatch = Filesystem::createHelloWorldPHPFile(); From 97dc013af31b0d6a339cfb5f8d88107cace7def7 Mon Sep 17 00:00:00 2001 From: "seregazhuk88@gmail.com" Date: Sat, 13 Jun 2020 23:02:14 +0300 Subject: [PATCH 02/59] Update scrutinizer PHP version --- .scrutinizer.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.scrutinizer.yml b/.scrutinizer.yml index dad16cd..c30852c 100644 --- a/.scrutinizer.yml +++ b/.scrutinizer.yml @@ -1,6 +1,6 @@ build: environment: - php: 7.2 + php: 7.4 nodes: analysis: From 5a3060231b8a75a3df03264eb4112d74b1e8c29c Mon Sep 17 00:00:00 2001 From: "seregazhuk88@gmail.com" Date: Sat, 13 Jun 2020 23:05:32 +0300 Subject: [PATCH 03/59] Fix arguments passing --- src/WatcherCommand.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/WatcherCommand.php b/src/WatcherCommand.php index 58ac603..093ec36 100644 --- a/src/WatcherCommand.php +++ b/src/WatcherCommand.php @@ -67,7 +67,7 @@ protected function execute(InputInterface $input, OutputInterface $output) $screen->showOptions($config->watchList()); $processRunner = new ProcessRunner($loop, $screen, $config->command()); - $this->addTerminationListeners($loop, $spinner, $filesystem, $processRunner); + $this->addTerminationListeners($loop, $spinner, $filesystem); $watcher = new Watcher($loop, $filesystem); $watcher->startWatching( From e046b1ebbba4eca2904bd7967951517b9a93d8a7 Mon Sep 17 00:00:00 2001 From: "seregazhuk88@gmail.com" Date: Sat, 13 Jun 2020 23:12:44 +0300 Subject: [PATCH 04/59] Reduce delay in tests --- tests/Feature/Helper/WatcherRunner.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Feature/Helper/WatcherRunner.php b/tests/Feature/Helper/WatcherRunner.php index 84faacc..9530f7f 100644 --- a/tests/Feature/Helper/WatcherRunner.php +++ b/tests/Feature/Helper/WatcherRunner.php @@ -8,7 +8,7 @@ final class WatcherRunner { public function run($scriptToRun, array $arguments = []): Process { - $arguments = array_merge($arguments, ['--delay', 0.25]); + $arguments = array_merge($arguments, ['--delay', 0.05]); $process = new Process(array_merge(['./php-watcher', $scriptToRun], $arguments)); $process->start(); From b69e2b5cce85a23693f8b23f07fbcbc081decd9d Mon Sep 17 00:00:00 2001 From: "seregazhuk88@gmail.com" Date: Sat, 13 Jun 2020 23:16:34 +0300 Subject: [PATCH 05/59] Update readme --- README.md | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index bd9c30b..10221cf 100644 --- a/README.md +++ b/README.md @@ -241,12 +241,23 @@ script crashes PHP-watcher will notify you about that. The watcher can use different strategies to monitor your file system changes. Under the hood it detects the environment and chooses the best suitable strategy. +### Resource-Watcher + By default, it uses [yosymfony/resource-watcher](https://github.com/yosymfony/resource-watcher -) which +) which is the slowest, and most resource intensive option, but it should work on all environments. +Under the hood it is constantly asking the filesystem whether there are new changes or not. + +### Fswatch + +[FsWatch](https://github.com/emcrisostomo/fswatch) is a cross-platform (Linux,Mac,Windows) file change monitor which will automatically + use the platforms native functionality when possible. Under the hood the filesystem notifies us + when any changes occur. If your system has fswatch installed this strategy will be used. + +**Has not been extensively tested.** ## Spinner -By default the watcher outputs a nice spinner which indicates that the process is running +By default, the watcher outputs a nice spinner which indicates that the process is running and watching your files. But if your system doesn't support ansi coded the watcher will try to detect it and disable the spinner. Or you can always disable the spinner manually with option '--no-spinner': From 07ddb08dcf6c953d4da9763031f0d2146a2f6466 Mon Sep 17 00:00:00 2001 From: "seregazhuk88@gmail.com" Date: Sat, 13 Jun 2020 23:17:20 +0300 Subject: [PATCH 06/59] Skip signals test --- tests/Feature/SignalTest.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/Feature/SignalTest.php b/tests/Feature/SignalTest.php index 7c95392..8d5f757 100644 --- a/tests/Feature/SignalTest.php +++ b/tests/Feature/SignalTest.php @@ -10,9 +10,9 @@ final class SignalTest extends WatcherTestCase /** @test */ public function it_sends_a_specified_signal_to_restart_the_app(): void { - if (!defined('SIGTERM')) { + //if (!defined('SIGTERM')) { $this->markTestSkipped('SIGTERM is not defined'); - } + //} $scriptToRun = Filesystem::createHelloWorldPHPFileWithSignalsHandling(); $this->watch($scriptToRun, ['--signal', 'SIGTERM', '--watch', Filesystem::fixturesDir()]); From b9b271d29253c00e998d458b1c63d76f970f408c Mon Sep 17 00:00:00 2001 From: "seregazhuk88@gmail.com" Date: Sat, 13 Jun 2020 23:17:46 +0300 Subject: [PATCH 07/59] Update changelog --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cf9a5e0..8a94ec8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,4 @@ -## 0.6.0 (2020-05-20) +## 0.6.0 (2020-06-14) * Fix: don't use child process for resource watching * Feature: add fswatch support * Fix: min required PHP version is set to 7.4 From dbd815338dfcecab7eb8469725f7fb3460c9ab7e Mon Sep 17 00:00:00 2001 From: "seregazhuk88@gmail.com" Date: Sat, 13 Jun 2020 23:29:34 +0300 Subject: [PATCH 08/59] Fix fixtures for testing --- tests/Feature/Helper/Filesystem.php | 2 +- tests/Feature/SignalTest.php | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/Feature/Helper/Filesystem.php b/tests/Feature/Helper/Filesystem.php index e1af2f1..ef2f974 100644 --- a/tests/Feature/Helper/Filesystem.php +++ b/tests/Feature/Helper/Filesystem.php @@ -75,7 +75,7 @@ public static function changeFileContentsWith(string $file, string $contents): v public static function fixturesDir(): string { - return self::FIXTURES_DIR; + return str_replace('tests/', '', self::FIXTURES_DIR); } private static function buildFilePath(string $filename): string diff --git a/tests/Feature/SignalTest.php b/tests/Feature/SignalTest.php index 8d5f757..7c95392 100644 --- a/tests/Feature/SignalTest.php +++ b/tests/Feature/SignalTest.php @@ -10,9 +10,9 @@ final class SignalTest extends WatcherTestCase /** @test */ public function it_sends_a_specified_signal_to_restart_the_app(): void { - //if (!defined('SIGTERM')) { + if (!defined('SIGTERM')) { $this->markTestSkipped('SIGTERM is not defined'); - //} + } $scriptToRun = Filesystem::createHelloWorldPHPFileWithSignalsHandling(); $this->watch($scriptToRun, ['--signal', 'SIGTERM', '--watch', Filesystem::fixturesDir()]); From adcfedcefb6501e4967836275bf8d051fb099659 Mon Sep 17 00:00:00 2001 From: "seregazhuk88@gmail.com" Date: Sat, 13 Jun 2020 23:30:36 +0300 Subject: [PATCH 09/59] Fix fixtures for testing --- tests/Feature/SignalTest.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/Feature/SignalTest.php b/tests/Feature/SignalTest.php index 7c95392..8d5f757 100644 --- a/tests/Feature/SignalTest.php +++ b/tests/Feature/SignalTest.php @@ -10,9 +10,9 @@ final class SignalTest extends WatcherTestCase /** @test */ public function it_sends_a_specified_signal_to_restart_the_app(): void { - if (!defined('SIGTERM')) { + //if (!defined('SIGTERM')) { $this->markTestSkipped('SIGTERM is not defined'); - } + //} $scriptToRun = Filesystem::createHelloWorldPHPFileWithSignalsHandling(); $this->watch($scriptToRun, ['--signal', 'SIGTERM', '--watch', Filesystem::fixturesDir()]); From f45c144b1353b4ecafcf24fbec14c7fb7c132f06 Mon Sep 17 00:00:00 2001 From: "seregazhuk88@gmail.com" Date: Sun, 14 Jun 2020 23:57:27 +0300 Subject: [PATCH 10/59] Fix fixtures folder --- tests/Feature/Helper/Filesystem.php | 2 +- tests/Feature/Helper/WatcherRunner.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/Feature/Helper/Filesystem.php b/tests/Feature/Helper/Filesystem.php index ef2f974..d181696 100644 --- a/tests/Feature/Helper/Filesystem.php +++ b/tests/Feature/Helper/Filesystem.php @@ -75,7 +75,7 @@ public static function changeFileContentsWith(string $file, string $contents): v public static function fixturesDir(): string { - return str_replace('tests/', '', self::FIXTURES_DIR); + return realpath(__DIR__ . '/../../../' . self::FIXTURES_DIR); } private static function buildFilePath(string $filename): string diff --git a/tests/Feature/Helper/WatcherRunner.php b/tests/Feature/Helper/WatcherRunner.php index 9530f7f..84faacc 100644 --- a/tests/Feature/Helper/WatcherRunner.php +++ b/tests/Feature/Helper/WatcherRunner.php @@ -8,7 +8,7 @@ final class WatcherRunner { public function run($scriptToRun, array $arguments = []): Process { - $arguments = array_merge($arguments, ['--delay', 0.05]); + $arguments = array_merge($arguments, ['--delay', 0.25]); $process = new Process(array_merge(['./php-watcher', $scriptToRun], $arguments)); $process->start(); From 9a72adcb8a920b708e67ac20f0db0618797c7b5f Mon Sep 17 00:00:00 2001 From: Sergey Zhuk Date: Thu, 23 Oct 2025 17:42:47 +0300 Subject: [PATCH 11/59] feat: use fswatch --- README.md | 20 +++---- composer.json | 1 + .../ChangesListenerInterface.php | 16 ++++++ .../ChokidarChangesListener.php} | 28 +++++++--- .../FSWatchChangesListener.php} | 51 +++++++++-------- src/Filesystem/Factory.php | 22 -------- .../FilesystemChangesListenerFactory.php | 35 ++++++++++++ src/Filesystem/WatchPath.php | 39 ------------- src/ProcessRunner.php | 2 +- src/Screen/Screen.php | 8 ++- src/SystemRequirements/SystemRequirements.php | 56 ------------------- .../SystemRequirementsChecker.php | 41 ++++++++++++++ .../SystemRequirementsNotMetException.php | 7 +++ src/Watcher.php | 4 +- src/WatcherCommand.php | 25 +++++---- ...st.php => ChokidarChangesListenerTest.php} | 6 +- tests/Feature/SignalTest.php | 2 +- tests/Unit/WatchPathTest.php | 55 ------------------ 18 files changed, 188 insertions(+), 230 deletions(-) create mode 100644 src/Filesystem/ChangesListener/ChangesListenerInterface.php rename src/Filesystem/{ChangesListener.php => ChangesListener/ChokidarChangesListener.php} (57%) rename src/Filesystem/{FsWatchBased/ChangesListener.php => ChangesListener/FSWatchChangesListener.php} (55%) delete mode 100644 src/Filesystem/Factory.php create mode 100644 src/Filesystem/FilesystemChangesListenerFactory.php delete mode 100644 src/Filesystem/WatchPath.php delete mode 100644 src/SystemRequirements/SystemRequirements.php create mode 100644 src/SystemRequirements/SystemRequirementsChecker.php create mode 100644 src/SystemRequirements/SystemRequirementsNotMetException.php rename tests/Feature/{ChangesListenerTest.php => ChokidarChangesListenerTest.php} (84%) delete mode 100644 tests/Unit/WatchPathTest.php diff --git a/README.md b/README.md index 3294d22..9db9191 100644 --- a/README.md +++ b/README.md @@ -56,9 +56,6 @@ composer require seregazhuk/php-watcher --dev ``` Locally installed you can run it with `vendor/bin/php-watcher`. -Under the hood, to watch filesystem changes, PHP-watcher uses JavaScript package [chokidar](https://github.com/paulmillr/chokidar). -At first run it will check and install it if required. - ## Usage All the examples assume you've installed the package globally. If you opted for the local installation prepend `vendor/bin/` everywhere where `php-watcher` is mentioned. @@ -242,20 +239,19 @@ script crashes PHP-watcher will notify you about that. The watcher can use different strategies to monitor your file system changes. Under the hood it detects the environment and chooses the best suitable strategy. -### Resource-Watcher - -By default, it uses [yosymfony/resource-watcher](https://github.com/yosymfony/resource-watcher -) which is the slowest, and most resource intensive option, but it should work on all environments. -Under the hood it is constantly asking the filesystem whether there are new changes or not. - ### Fswatch -[FsWatch](https://github.com/emcrisostomo/fswatch) is a cross-platform (Linux,Mac,Windows) file change monitor which will automatically - use the platforms native functionality when possible. Under the hood the filesystem notifies us - when any changes occur. If your system has fswatch installed this strategy will be used. +[FsWatch](https://github.com/emcrisostomo/fswatch) is a cross-platform (Linux,Mac,Windows) file change monitor that will automatically +use the platforms native functionality when possible. Under the hood the filesystem notifies us +when any changes occur. If your system has fswatch installed, this strategy will be used. **Has not been extensively tested.** +### Chokidar + +[Chokidar](https://github.com/paulmillr/chokidar) is a JavaScript package for watching file and directory changes. +At first run the watcher will check if Node.js is available in the system. If it is, it will install chokidar into the project. + ## Spinner By default, the watcher outputs a nice spinner which indicates that the process is running diff --git a/composer.json b/composer.json index 4224dfc..287e221 100644 --- a/composer.json +++ b/composer.json @@ -31,6 +31,7 @@ "react/child-process": "^0.6.1", "react/event-loop": "^1.1", "react/stream": "^1.0.0", + "seregazhuk/reactphp-fswatch": "^1.0.0", "spatie/file-system-watcher": "^1.2", "symfony/console": "^6.0", "symfony/finder": "^6.0", diff --git a/src/Filesystem/ChangesListener/ChangesListenerInterface.php b/src/Filesystem/ChangesListener/ChangesListenerInterface.php new file mode 100644 index 0000000..815328c --- /dev/null +++ b/src/Filesystem/ChangesListener/ChangesListenerInterface.php @@ -0,0 +1,16 @@ +find('node'), - realpath(__DIR__.'/../../bin/file-watcher.js'), + realpath(__DIR__.'/../../../bin/file-watcher.js'), json_encode($watchList->getPaths()), json_encode($watchList->getIgnored()), json_encode($watchList->getFileExtensions()), ]; - $process = new Process(command: $command); - $process->start(); + $this->process = new Process(command: $command); + $this->process->start(); $this->loop->addPeriodicTimer( self::INTERVAL, - function () use ($process): void { - $output = $process->getIncrementalOutput(); + function (): void { + $output = $this->process->getIncrementalOutput(); if ($output !== '') { $this->emit('change'); } @@ -44,4 +46,16 @@ public function onChange(callable $callback): void { $this->on('change', $callback); } + + public function stop(): void + { + if ($this->process instanceof Process && $this->process->isRunning()) { + $this->process->stop(); + } + } + + public function getName(): string + { + return 'chokidar'; + } } diff --git a/src/Filesystem/FsWatchBased/ChangesListener.php b/src/Filesystem/ChangesListener/FSWatchChangesListener.php similarity index 55% rename from src/Filesystem/FsWatchBased/ChangesListener.php rename to src/Filesystem/ChangesListener/FSWatchChangesListener.php index dab06f9..09fe04c 100644 --- a/src/Filesystem/FsWatchBased/ChangesListener.php +++ b/src/Filesystem/ChangesListener/FSWatchChangesListener.php @@ -2,34 +2,32 @@ declare(strict_types=1); -namespace seregazhuk\PhpWatcher\Filesystem\FsWatchBased; +namespace seregazhuk\PhpWatcher\Filesystem\ChangesListener; use Evenement\EventEmitter; use React\EventLoop\LoopInterface; use seregazhuk\PhpWatcher\Config\WatchList; -use seregazhuk\PhpWatcher\Filesystem\ChangesListener as ChangesListenerInterface; use Seregazhuk\ReactFsWatch\FsWatch; -final class ChangesListener extends EventEmitter implements ChangesListenerInterface +final class FSWatchChangesListener extends EventEmitter implements ChangesListenerInterface { - private FsWatch $fsWatch; + private ?FsWatch $fsWatch = null; - public function __construct(WatchList $watchList, LoopInterface $loop) - { - $this->fsWatch = new FsWatch($this->makeOptions($watchList), $loop); - } + public function __construct(private readonly LoopInterface $loop) {} public static function isAvailable(): bool { return FsWatch::isAvailable(); } - public function start(): void + public function start(WatchList $watchList): void { + $this->fsWatch = new FsWatch($this->makeOptions($watchList), $this->loop); + $this->fsWatch->run(); $this->fsWatch->on( 'change', - function () { + function (): void { $this->emit('change'); } ); @@ -42,7 +40,9 @@ public function onChange(callable $callback): void public function stop(): void { - $this->fsWatch->stop(); + if ($this->fsWatch instanceof FsWatch) { + $this->fsWatch->stop(); + } } private function makeOptions(WatchList $watchList): string @@ -50,17 +50,17 @@ private function makeOptions(WatchList $watchList): string $options = []; // first come paths - if ($watchList->paths()) { - $options[] = implode(' ', $watchList->paths()); + if ($watchList->getPaths() !== []) { + $options[] = implode(' ', $watchList->getPaths()); } // then we ignore - if ($watchList->ignore()) { - $options[] = '-e ' . implode(' ', $watchList->ignore()); + if ($watchList->getIgnored() !== []) { + $options[] = '-e '.implode(' ', $watchList->getIgnored()); } // then include - if ($watchList->fileExtensions()) { + if ($watchList->getFileExtensions() !== []) { $options = array_merge($options, $this->makeIncludeOptions($watchList)); } @@ -69,21 +69,28 @@ private function makeOptions(WatchList $watchList): string return implode(' ', $options); } + /** + * @return string[] + */ private function makeIncludeOptions(WatchList $watchList): array { $options = []; // Before including we need to ignore everything - if (empty($watchList->ignore())) { + if ($watchList->getIgnored() === []) { $options[] = '-e ".*"'; } $regexpWithExtensions = array_map( - static function ($extension) { - return str_replace('*.', '.', $extension) . '$'; - }, - $watchList->fileExtensions() + static fn ($extension) => str_replace('*.', '.', $extension).'$', + $watchList->getFileExtensions() ); - $options[] = '-i ' . implode(' ', $regexpWithExtensions); + $options[] = '-i '.implode(' ', $regexpWithExtensions); + return $options; } + + public function getName(): string + { + return 'fswatch'; + } } diff --git a/src/Filesystem/Factory.php b/src/Filesystem/Factory.php deleted file mode 100644 index 8e43427..0000000 --- a/src/Filesystem/Factory.php +++ /dev/null @@ -1,22 +0,0 @@ -comment('Chokidar is not installed in the project.'); + $screen->comment('Installing chokidar...'); + SystemRequirementsChecker::installChokidar(); + } + + return new ChokidarChangesListener($loop); + } + + throw new SystemRequirementsNotMetException('Neither Node.js nor fswatch are installed.'); + } +} diff --git a/src/Filesystem/WatchPath.php b/src/Filesystem/WatchPath.php deleted file mode 100644 index ca380f3..0000000 --- a/src/Filesystem/WatchPath.php +++ /dev/null @@ -1,39 +0,0 @@ -isDirectory()) { - return true; - } - - return ! file_exists($this->pattern); - } - - private function directoryPart(): string - { - return pathinfo($this->pattern, PATHINFO_DIRNAME); - } - - public function fileName(): string - { - return pathinfo($this->pattern, PATHINFO_BASENAME); - } - - private function isDirectory(): bool - { - return is_dir($this->pattern); - } - - public function path(): string - { - return $this->isDirectory() ? $this->pattern : $this->directoryPart(); - } -} diff --git a/src/ProcessRunner.php b/src/ProcessRunner.php index d2fac37..2f87ebf 100644 --- a/src/ProcessRunner.php +++ b/src/ProcessRunner.php @@ -35,7 +35,7 @@ public function stop(int $signal): void public function restart(float $delayToRestart): void { - $this->screen->restarting($this->process->getCommand()); + $this->screen->restarting(); $this->loop->addTimer($delayToRestart, $this->start(...)); } diff --git a/src/Screen/Screen.php b/src/Screen/Screen.php index 050762e..5defb90 100644 --- a/src/Screen/Screen.php +++ b/src/Screen/Screen.php @@ -8,6 +8,7 @@ use React\EventLoop\LoopInterface; use seregazhuk\PhpWatcher\Config\WatchList; use seregazhuk\PhpWatcher\ConsoleApplication; +use seregazhuk\PhpWatcher\Filesystem\ChangesListener\ChangesListenerInterface; use Symfony\Component\Console\Style\SymfonyStyle; final class Screen @@ -59,7 +60,7 @@ public function start(string $command): void $this->info(sprintf('starting `%s`', trim($command))); } - public function restarting(?string $command = null): void + public function restarting(): void { $this->spinner->erase(); $this->output->writeln(''); @@ -92,4 +93,9 @@ private function message(string $text): string { return sprintf('[%s] %s', ConsoleApplication::NAME, $text); } + + public function showFilesystemListener(ChangesListenerInterface $filesystemListener): void + { + $this->comment('using filesystem listener: '.$filesystemListener->getName()); + } } diff --git a/src/SystemRequirements/SystemRequirements.php b/src/SystemRequirements/SystemRequirements.php deleted file mode 100644 index c27086f..0000000 --- a/src/SystemRequirements/SystemRequirements.php +++ /dev/null @@ -1,56 +0,0 @@ -isNodeJsInstalled() === false) { - $this->screen->warning('Node.js is not installed.'); - $this->screen->warning('Please install it from https://nodejs.org/en/download/'); - - return false; - } - - if ($this->isChokidarInstalled() === false) { - $this->screen->comment('Chokidar is not installed in the project.'); - $this->screen->comment('Installing chokidar...'); - $this->installChokidar(); - } - - return true; - } - - public function isNodeJsInstalled(): bool - { - $process = new Process(command: ['node', '-v']); - $process->start(); - $process->wait(); - - return $process->getExitCode() === 0; - } - - public function isChokidarInstalled(): bool - { - $process = new Process(command: ['npm', 'list', 'chokidar']); - $process->start(); - $process->wait(); - - return $process->getExitCode() === 0 && str_contains($process->getOutput(), 'chokidar'); - } - - public function installChokidar(): void - { - $process = new Process(command: ['npm', 'install', 'chokidar']); - $process->start(); - $process->wait(); - } -} diff --git a/src/SystemRequirements/SystemRequirementsChecker.php b/src/SystemRequirements/SystemRequirementsChecker.php new file mode 100644 index 0000000..d68a889 --- /dev/null +++ b/src/SystemRequirements/SystemRequirementsChecker.php @@ -0,0 +1,41 @@ +start(); + $process->wait(); + + return $process->getExitCode() === 0; + } + + public static function isChokidarInstalled(): bool + { + $process = new Process(command: ['npm', 'list', 'chokidar']); + $process->start(); + $process->wait(); + + return $process->getExitCode() === 0 && str_contains($process->getOutput(), 'chokidar'); + } + + public static function installChokidar(): void + { + $process = new Process(command: ['npm', 'install', 'chokidar']); + $process->start(); + $process->wait(); + } +} diff --git a/src/SystemRequirements/SystemRequirementsNotMetException.php b/src/SystemRequirements/SystemRequirementsNotMetException.php new file mode 100644 index 0000000..19ab0c1 --- /dev/null +++ b/src/SystemRequirements/SystemRequirementsNotMetException.php @@ -0,0 +1,7 @@ +spinnerDisabled()); $screen = new Screen(new SymfonyStyle($input, $output), $spinner); - $requirements = new SystemRequirements($screen); - if ($requirements->check() === false) { + $loop = Loop::get(); + try { + $filesystemListener = FilesystemChangesListenerFactory::create($loop, $screen); + } catch (SystemRequirementsNotMetException $e) { + $screen->warning($e->getMessage()); + return Command::FAILURE; } + $this->addTerminationListeners($loop, $spinner, $filesystemListener); - $loop = Loop::get(); - $this->addTerminationListeners($loop, $spinner); - $filesystem = new ChangesListener($loop); $screen->showOptions($config->watchList()); + $screen->showFilesystemListener($filesystemListener); $processRunner = new ProcessRunner($loop, $screen, $config->command()); - $watcher = new Watcher($loop, $filesystem); + $watcher = new Watcher($loop, $filesystemListener); $watcher->startWatching( $processRunner, $config->watchList(), @@ -88,10 +92,11 @@ protected function execute(InputInterface $input, OutputInterface $output): int /** * When terminating the watcher, we need to manually restore the cursor after the spinner. */ - private function addTerminationListeners(LoopInterface $loop, SpinnerInterface $spinner): void + private function addTerminationListeners(LoopInterface $loop, SpinnerInterface $spinner, ChangesListenerInterface $changesListener): void { - $func = static function (int $signal) use ($spinner): never { + $func = static function (int $signal) use ($spinner, $changesListener): never { $spinner->end(); + $changesListener->stop(); exit($signal); }; diff --git a/tests/Feature/ChangesListenerTest.php b/tests/Feature/ChokidarChangesListenerTest.php similarity index 84% rename from tests/Feature/ChangesListenerTest.php rename to tests/Feature/ChokidarChangesListenerTest.php index 7843f4d..38796be 100644 --- a/tests/Feature/ChangesListenerTest.php +++ b/tests/Feature/ChokidarChangesListenerTest.php @@ -8,13 +8,13 @@ use PHPUnit\Framework\TestCase; use React\EventLoop\Loop; use seregazhuk\PhpWatcher\Config\WatchList; -use seregazhuk\PhpWatcher\Filesystem\ChangesListener; +use seregazhuk\PhpWatcher\Filesystem\ChangesListener\ChokidarChangesListener; use seregazhuk\PhpWatcher\Tests\Feature\Helper\Filesystem; use seregazhuk\PhpWatcher\Tests\Feature\Helper\WithFilesystem; use function React\Async\delay; -final class ChangesListenerTest extends TestCase +final class ChokidarChangesListenerTest extends TestCase { use WithFilesystem; @@ -28,7 +28,7 @@ protected function tearDown(): void public function it_emits_change_event_on_changes(): void { $loop = Loop::get(); - $listener = new ChangesListener($loop); + $listener = new ChokidarChangesListener($loop); $listener->start(new WatchList([Filesystem::fixturesDir()])); $loop->addTimer(1, Filesystem::createHelloWorldPHPFile(...)); diff --git a/tests/Feature/SignalTest.php b/tests/Feature/SignalTest.php index 0764e1d..31f7692 100644 --- a/tests/Feature/SignalTest.php +++ b/tests/Feature/SignalTest.php @@ -15,7 +15,7 @@ public function it_sends_a_specified_signal_to_restart_the_app(): void { if (! defined('SIGTERM')) { $this->markTestSkipped('SIGTERM is not defined'); - //} + } $scriptToRun = Filesystem::createHelloWorldPHPFileWithSignalsHandling(); $this->watch($scriptToRun, ['--signal', 'SIGTERM', '--watch', Filesystem::fixturesDir()]); diff --git a/tests/Unit/WatchPathTest.php b/tests/Unit/WatchPathTest.php deleted file mode 100644 index 4d3585b..0000000 --- a/tests/Unit/WatchPathTest.php +++ /dev/null @@ -1,55 +0,0 @@ -assertEquals('/root/test', $path->path()); - } - - /** @test */ - public function it_provides_a_directory_path_for_a_directory(): void - { - $path = new WatchPath('/root/test'); - $this->assertEquals('/root', $path->path()); - } - - /** @test */ - public function it_provides_a_filename_for_a_file(): void - { - $path = new WatchPath('/root/test.txt'); - $this->assertEquals('test.txt', $path->fileName()); - } - - /** @test */ - public function it_provides_a_pattern_path_for_a_pattern(): void - { - $path = new WatchPath('/root/test.*'); - $this->assertEquals('test.*', $path->fileName()); - } - - /** @test */ - public function it_can_detect_pattern_or_a_file(): void - { - $path = new WatchPath('/root/test.*'); - $this->assertTrue($path->isFileOrPattern()); - - $path = new WatchPath('/root/test.txt'); - $this->assertTrue($path->isFileOrPattern()); - - $path = new WatchPath('/root/*.txt'); - $this->assertTrue($path->isFileOrPattern()); - - $path = new WatchPath(__DIR__); - $this->assertFalse($path->isFileOrPattern()); - } -} From 3e59d284047f22ba72234d30f15bd8bf622e0652 Mon Sep 17 00:00:00 2001 From: Sergey Zhuk Date: Thu, 23 Oct 2025 22:46:37 +0300 Subject: [PATCH 12/59] feat: use fswatch --- .github/workflows/tests.yml | 6 +++ .travis.yml | 19 -------- composer.json | 4 +- .../ChokidarChangesListener.php | 8 +++- .../FSWatchChangesListener.php | 12 +---- .../FilesystemChangesListenerFactory.php | 2 +- tests/Feature/ChokidarChangesListenerTest.php | 12 ++--- tests/Feature/FsWatchChangesListenerTest.php | 44 +++++++++++++++++++ 8 files changed, 66 insertions(+), 41 deletions(-) delete mode 100644 .travis.yml create mode 100644 tests/Feature/FsWatchChangesListenerTest.php diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 072ad5f..4b6e8fb 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -41,6 +41,12 @@ jobs: php-version: ${{ matrix.php }} extensions: dom, sockets, grpc, curl ${{ matrix.extensions-suffix }} + - name: Install fswatch + run: | + sudo add-apt-repository ppa:hadret/fswatch \ + sudo apt-get update \ + sudo apt-get install fswatch + - name: Validate composer.json and composer.lock run: composer validate --strict diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 2b30dbf..0000000 --- a/.travis.yml +++ /dev/null @@ -1,19 +0,0 @@ -language: php - -jobs: - include: - - stage: "PHP7.4 - lowest" - php: 7.4 - script: - - composer update -n --prefer-dist --no-suggest - - composer dump-autoload - - composer ci:tests - - composer ci:php:psalm - - - stage: "PHP8.0 - highest" - php: 8.0 - script: - - composer update -n --prefer-dist --no-suggest - - composer dump-autoload - - composer ci:tests - - composer ci:php:psalm diff --git a/composer.json b/composer.json index 287e221..f08ba3a 100644 --- a/composer.json +++ b/composer.json @@ -31,10 +31,10 @@ "react/child-process": "^0.6.1", "react/event-loop": "^1.1", "react/stream": "^1.0.0", - "seregazhuk/reactphp-fswatch": "^1.0.0", - "spatie/file-system-watcher": "^1.2", + "seregazhuk/reactphp-fswatch": "^1.1.0", "symfony/console": "^6.0", "symfony/finder": "^6.0", + "symfony/process": "^6.0", "symfony/yaml": "^6.0" }, "autoload": { diff --git a/src/Filesystem/ChangesListener/ChokidarChangesListener.php b/src/Filesystem/ChangesListener/ChokidarChangesListener.php index 5ee2615..235ca1b 100644 --- a/src/Filesystem/ChangesListener/ChokidarChangesListener.php +++ b/src/Filesystem/ChangesListener/ChokidarChangesListener.php @@ -6,6 +6,7 @@ use Evenement\EventEmitter; use React\EventLoop\LoopInterface; +use React\EventLoop\TimerInterface; use seregazhuk\PhpWatcher\Config\WatchList; use Symfony\Component\Process\ExecutableFinder; use Symfony\Component\Process\Process; @@ -15,6 +16,7 @@ final class ChokidarChangesListener extends EventEmitter implements ChangesListe private const INTERVAL = 0.15; private ?Process $process = null; + private ?TimerInterface $timer = null; public function __construct(private readonly LoopInterface $loop) {} @@ -31,7 +33,7 @@ public function start(WatchList $watchList): void $this->process = new Process(command: $command); $this->process->start(); - $this->loop->addPeriodicTimer( + $this->timer = $this->loop->addPeriodicTimer( self::INTERVAL, function (): void { $output = $this->process->getIncrementalOutput(); @@ -52,6 +54,10 @@ public function stop(): void if ($this->process instanceof Process && $this->process->isRunning()) { $this->process->stop(); } + + if ($this->timer !== null) { + $this->loop->cancelTimer($this->timer); + } } public function getName(): string diff --git a/src/Filesystem/ChangesListener/FSWatchChangesListener.php b/src/Filesystem/ChangesListener/FSWatchChangesListener.php index 09fe04c..b049f28 100644 --- a/src/Filesystem/ChangesListener/FSWatchChangesListener.php +++ b/src/Filesystem/ChangesListener/FSWatchChangesListener.php @@ -5,7 +5,6 @@ namespace seregazhuk\PhpWatcher\Filesystem\ChangesListener; use Evenement\EventEmitter; -use React\EventLoop\LoopInterface; use seregazhuk\PhpWatcher\Config\WatchList; use Seregazhuk\ReactFsWatch\FsWatch; @@ -13,8 +12,6 @@ final class FSWatchChangesListener extends EventEmitter implements ChangesListen { private ?FsWatch $fsWatch = null; - public function __construct(private readonly LoopInterface $loop) {} - public static function isAvailable(): bool { return FsWatch::isAvailable(); @@ -22,15 +19,10 @@ public static function isAvailable(): bool public function start(WatchList $watchList): void { - $this->fsWatch = new FsWatch($this->makeOptions($watchList), $this->loop); + $this->fsWatch = new FsWatch($this->makeOptions($watchList)); $this->fsWatch->run(); - $this->fsWatch->on( - 'change', - function (): void { - $this->emit('change'); - } - ); + $this->fsWatch->onChange(fn() => $this->emit('change')); } public function onChange(callable $callback): void diff --git a/src/Filesystem/FilesystemChangesListenerFactory.php b/src/Filesystem/FilesystemChangesListenerFactory.php index 2df0a0d..f2396d4 100644 --- a/src/Filesystem/FilesystemChangesListenerFactory.php +++ b/src/Filesystem/FilesystemChangesListenerFactory.php @@ -17,7 +17,7 @@ final class FilesystemChangesListenerFactory public static function create(LoopInterface $loop, Screen $screen): ChangesListenerInterface { if (SystemRequirementsChecker::isFsWatchAvailable()) { - return new FsWatchChangesListener($loop); + return new FsWatchChangesListener(); } if (SystemRequirementsChecker::isNodeJsInstalled()) { diff --git a/tests/Feature/ChokidarChangesListenerTest.php b/tests/Feature/ChokidarChangesListenerTest.php index 38796be..bab0bfb 100644 --- a/tests/Feature/ChokidarChangesListenerTest.php +++ b/tests/Feature/ChokidarChangesListenerTest.php @@ -12,18 +12,13 @@ use seregazhuk\PhpWatcher\Tests\Feature\Helper\Filesystem; use seregazhuk\PhpWatcher\Tests\Feature\Helper\WithFilesystem; +use function React\Async\async; use function React\Async\delay; final class ChokidarChangesListenerTest extends TestCase { use WithFilesystem; - protected function tearDown(): void - { - Loop::get()->stop(); - parent::tearDown(); - } - #[Test] public function it_emits_change_event_on_changes(): void { @@ -33,10 +28,11 @@ public function it_emits_change_event_on_changes(): void $loop->addTimer(1, Filesystem::createHelloWorldPHPFile(...)); $eventWasEmitted = false; - $listener->on('change', static function () use (&$eventWasEmitted): void { + $listener->onChange(function () use (&$eventWasEmitted): void { $eventWasEmitted = true; }); - delay(4); // to be sure changes have been detected + delay(4); $this->assertTrue($eventWasEmitted, '"change" event should be emitted'); + $listener->stop(); } } diff --git a/tests/Feature/FsWatchChangesListenerTest.php b/tests/Feature/FsWatchChangesListenerTest.php new file mode 100644 index 0000000..58487eb --- /dev/null +++ b/tests/Feature/FsWatchChangesListenerTest.php @@ -0,0 +1,44 @@ +markTestSkipped('fswatch is not available'); + } + + $loop = Loop::get(); + $loop->addTimer(1, async(Filesystem::createHelloWorldPHPFile(...))); + + $listener = new FSWatchChangesListener(); + $listener->start(new WatchList([Filesystem::fixturesDir()])); + + $listener->onChange(function (): void { + $this->assertTrue(true); + }); + delay(4); + $loop->stop(); + } +} From 0cfdc51552d89b6bf26043c05dce41ca05e95ff4 Mon Sep 17 00:00:00 2001 From: Sergey Zhuk Date: Thu, 23 Oct 2025 22:49:22 +0300 Subject: [PATCH 13/59] feat: use fswatch --- .github/workflows/tests.yml | 1 - src/Filesystem/ChangesListener/ChokidarChangesListener.php | 3 ++- src/Filesystem/ChangesListener/FSWatchChangesListener.php | 2 +- src/Filesystem/FilesystemChangesListenerFactory.php | 2 +- tests/Feature/ChokidarChangesListenerTest.php | 1 - tests/Feature/FsWatchChangesListenerTest.php | 7 ++----- 6 files changed, 6 insertions(+), 10 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 4b6e8fb..2315ce4 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -43,7 +43,6 @@ jobs: - name: Install fswatch run: | - sudo add-apt-repository ppa:hadret/fswatch \ sudo apt-get update \ sudo apt-get install fswatch diff --git a/src/Filesystem/ChangesListener/ChokidarChangesListener.php b/src/Filesystem/ChangesListener/ChokidarChangesListener.php index 235ca1b..5f580bd 100644 --- a/src/Filesystem/ChangesListener/ChokidarChangesListener.php +++ b/src/Filesystem/ChangesListener/ChokidarChangesListener.php @@ -16,6 +16,7 @@ final class ChokidarChangesListener extends EventEmitter implements ChangesListe private const INTERVAL = 0.15; private ?Process $process = null; + private ?TimerInterface $timer = null; public function __construct(private readonly LoopInterface $loop) {} @@ -55,7 +56,7 @@ public function stop(): void $this->process->stop(); } - if ($this->timer !== null) { + if ($this->timer instanceof \React\EventLoop\TimerInterface) { $this->loop->cancelTimer($this->timer); } } diff --git a/src/Filesystem/ChangesListener/FSWatchChangesListener.php b/src/Filesystem/ChangesListener/FSWatchChangesListener.php index b049f28..5be4701 100644 --- a/src/Filesystem/ChangesListener/FSWatchChangesListener.php +++ b/src/Filesystem/ChangesListener/FSWatchChangesListener.php @@ -22,7 +22,7 @@ public function start(WatchList $watchList): void $this->fsWatch = new FsWatch($this->makeOptions($watchList)); $this->fsWatch->run(); - $this->fsWatch->onChange(fn() => $this->emit('change')); + $this->fsWatch->onChange(fn () => $this->emit('change')); } public function onChange(callable $callback): void diff --git a/src/Filesystem/FilesystemChangesListenerFactory.php b/src/Filesystem/FilesystemChangesListenerFactory.php index f2396d4..def9f09 100644 --- a/src/Filesystem/FilesystemChangesListenerFactory.php +++ b/src/Filesystem/FilesystemChangesListenerFactory.php @@ -17,7 +17,7 @@ final class FilesystemChangesListenerFactory public static function create(LoopInterface $loop, Screen $screen): ChangesListenerInterface { if (SystemRequirementsChecker::isFsWatchAvailable()) { - return new FsWatchChangesListener(); + return new FsWatchChangesListener; } if (SystemRequirementsChecker::isNodeJsInstalled()) { diff --git a/tests/Feature/ChokidarChangesListenerTest.php b/tests/Feature/ChokidarChangesListenerTest.php index bab0bfb..825666f 100644 --- a/tests/Feature/ChokidarChangesListenerTest.php +++ b/tests/Feature/ChokidarChangesListenerTest.php @@ -12,7 +12,6 @@ use seregazhuk\PhpWatcher\Tests\Feature\Helper\Filesystem; use seregazhuk\PhpWatcher\Tests\Feature\Helper\WithFilesystem; -use function React\Async\async; use function React\Async\delay; final class ChokidarChangesListenerTest extends TestCase diff --git a/tests/Feature/FsWatchChangesListenerTest.php b/tests/Feature/FsWatchChangesListenerTest.php index 58487eb..b997304 100644 --- a/tests/Feature/FsWatchChangesListenerTest.php +++ b/tests/Feature/FsWatchChangesListenerTest.php @@ -12,10 +12,7 @@ use seregazhuk\PhpWatcher\Tests\Feature\Helper\Filesystem; use seregazhuk\PhpWatcher\Tests\Feature\Helper\WithFilesystem; -use Seregazhuk\ReactFsWatch\Change; - use function React\Async\async; -use function React\Async\await; use function React\Async\delay; final class FsWatchChangesListenerTest extends TestCase @@ -25,14 +22,14 @@ final class FsWatchChangesListenerTest extends TestCase #[Test] public function it_emits_change_event_on_changes(): void { - if (!FSWatchChangesListener::isAvailable()) { + if (! FSWatchChangesListener::isAvailable()) { $this->markTestSkipped('fswatch is not available'); } $loop = Loop::get(); $loop->addTimer(1, async(Filesystem::createHelloWorldPHPFile(...))); - $listener = new FSWatchChangesListener(); + $listener = new FSWatchChangesListener; $listener->start(new WatchList([Filesystem::fixturesDir()])); $listener->onChange(function (): void { From 58181a1d1a86dff41424734f5a184d701fa8629a Mon Sep 17 00:00:00 2001 From: Sergey Zhuk Date: Thu, 23 Oct 2025 22:52:26 +0300 Subject: [PATCH 14/59] feat: use fswatch --- .github/workflows/tests.yml | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 2315ce4..6c358c4 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -42,9 +42,7 @@ jobs: extensions: dom, sockets, grpc, curl ${{ matrix.extensions-suffix }} - name: Install fswatch - run: | - sudo apt-get update \ - sudo apt-get install fswatch + run: sudo apt-get install fswatch - name: Validate composer.json and composer.lock run: composer validate --strict From 6e9c9b6a3007c13ca9cead6bcab7047bbfb4b1c2 Mon Sep 17 00:00:00 2001 From: Sergey Zhuk Date: Thu, 23 Oct 2025 22:53:57 +0300 Subject: [PATCH 15/59] feat: use fswatch --- .github/workflows/tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 6c358c4..99936ff 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -60,4 +60,4 @@ jobs: - name: Run tests with Phpunit run: | - XDEBUG_MODE=coverage php vendor/bin/phpunit --testsuite unit + XDEBUG_MODE=coverage php vendor/bin/phpunit From 33b67ad12d4515924d1668a2dea1ee1f4f93ff65 Mon Sep 17 00:00:00 2001 From: Sergey Zhuk Date: Thu, 23 Oct 2025 22:57:32 +0300 Subject: [PATCH 16/59] feat: use fswatch --- .github/workflows/tests.yml | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 99936ff..33a7f3b 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -35,15 +35,18 @@ jobs: - name: Check Out Code uses: actions/checkout@v4 + - name: Install fswatch + run: sudo apt-get install fswatch + + - name: Setup node + uses: actions/setup-node@v6 + - name: Setup PHP ${{ matrix.php }} uses: shivammathur/setup-php@v2 with: php-version: ${{ matrix.php }} extensions: dom, sockets, grpc, curl ${{ matrix.extensions-suffix }} - - name: Install fswatch - run: sudo apt-get install fswatch - - name: Validate composer.json and composer.lock run: composer validate --strict From 3dfff6166bfb7bf12a00a07f9b304aecca445ec2 Mon Sep 17 00:00:00 2001 From: Sergey Zhuk Date: Thu, 23 Oct 2025 22:58:38 +0300 Subject: [PATCH 17/59] feat: use fswatch --- .github/workflows/tests.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 33a7f3b..8156144 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -41,6 +41,9 @@ jobs: - name: Setup node uses: actions/setup-node@v6 + - name: Install chokidar + run: npm install chokidar + - name: Setup PHP ${{ matrix.php }} uses: shivammathur/setup-php@v2 with: From ac122eb5021357fc10660809ff228e0e1595d8db Mon Sep 17 00:00:00 2001 From: Sergey Zhuk Date: Fri, 24 Oct 2025 07:43:39 +0300 Subject: [PATCH 18/59] feat: use fswatch --- .github/workflows/tests-fswatch.yml | 63 +++++++++++++++++++ .../workflows/{tests.yml => tests-nodejs.yml} | 5 +- 2 files changed, 64 insertions(+), 4 deletions(-) create mode 100644 .github/workflows/tests-fswatch.yml rename .github/workflows/{tests.yml => tests-nodejs.yml} (91%) diff --git a/.github/workflows/tests-fswatch.yml b/.github/workflows/tests-fswatch.yml new file mode 100644 index 0000000..7866fb8 --- /dev/null +++ b/.github/workflows/tests-fswatch.yml @@ -0,0 +1,63 @@ +name: 'Tests' + +on: + push: + branches: + - main + paths-ignore: + - '.gitignore' + - 'CHANGELOG.md' + - 'LICENSE' + - 'README.md' + + pull_request: + paths-ignore: + - '.gitignore' + - 'CHANGELOG.md' + - 'LICENSE' + - 'README.md' + +jobs: + tests: + name: Tests with FsWatch (PHP ${{ matrix.php }}, OS ${{ matrix.os }}) + + runs-on: ${{ matrix.os }} + + strategy: + matrix: + os: [ubuntu-latest] + php: [8.1, 8.2, 8.3, 8.4] + + env: + extensions: xdebug + + steps: + - name: Check Out Code + uses: actions/checkout@v4 + + - name: Install fswatch + run: sudo apt-get install fswatch + + - name: Setup PHP ${{ matrix.php }} + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php }} + extensions: dom, sockets, grpc, curl ${{ matrix.extensions-suffix }} + + - name: Validate composer.json and composer.lock + run: composer validate --strict + + - name: Install dependencies with composer + uses: ramsey/composer-install@v3 + with: + dependency-versions: ${{ matrix.dependencies }} + + - name: Validate lowest dependencies + if: matrix.dependencies == 'lowest' && matrix.php == '8.1' + env: + COMPOSER_POOL_OPTIMIZER: 0 + run: vendor/bin/validate-prefer-lowest + + - name: Run tests with Phpunit + run: | + XDEBUG_MODE=coverage php vendor/bin/phpunit diff --git a/.github/workflows/tests.yml b/.github/workflows/tests-nodejs.yml similarity index 91% rename from .github/workflows/tests.yml rename to .github/workflows/tests-nodejs.yml index 8156144..e0e704d 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests-nodejs.yml @@ -19,7 +19,7 @@ on: jobs: tests: - name: Tests (PHP ${{ matrix.php }}, OS ${{ matrix.os }}) + name: Tests with Chokidar (PHP ${{ matrix.php }}, OS ${{ matrix.os }}) runs-on: ${{ matrix.os }} @@ -35,9 +35,6 @@ jobs: - name: Check Out Code uses: actions/checkout@v4 - - name: Install fswatch - run: sudo apt-get install fswatch - - name: Setup node uses: actions/setup-node@v6 From f4b188324333bfab4a5534a0a88bf87991eed116 Mon Sep 17 00:00:00 2001 From: Sergey Zhuk Date: Fri, 24 Oct 2025 11:52:26 +0300 Subject: [PATCH 19/59] feat: use fswatch --- .../{tests-nodejs.yml => tests-chokidar.yml} | 3 ++- .github/workflows/tests-fswatch.yml | 3 ++- .../ChangesListener/ChokidarChangesListener.php | 2 +- src/ProcessRunner.php | 2 +- src/Screen/Screen.php | 12 +++++++++++- src/WatcherCommand.php | 9 ++++----- 6 files changed, 21 insertions(+), 10 deletions(-) rename .github/workflows/{tests-nodejs.yml => tests-chokidar.yml} (96%) diff --git a/.github/workflows/tests-nodejs.yml b/.github/workflows/tests-chokidar.yml similarity index 96% rename from .github/workflows/tests-nodejs.yml rename to .github/workflows/tests-chokidar.yml index e0e704d..f8c56b9 100644 --- a/.github/workflows/tests-nodejs.yml +++ b/.github/workflows/tests-chokidar.yml @@ -1,4 +1,4 @@ -name: 'Tests' +name: 'Tests with Chokidar' on: push: @@ -19,6 +19,7 @@ on: jobs: tests: + timeout-minutes: 5 name: Tests with Chokidar (PHP ${{ matrix.php }}, OS ${{ matrix.os }}) runs-on: ${{ matrix.os }} diff --git a/.github/workflows/tests-fswatch.yml b/.github/workflows/tests-fswatch.yml index 7866fb8..ed3d988 100644 --- a/.github/workflows/tests-fswatch.yml +++ b/.github/workflows/tests-fswatch.yml @@ -1,4 +1,4 @@ -name: 'Tests' +name: 'Tests with FsWatch' on: push: @@ -19,6 +19,7 @@ on: jobs: tests: + timeout-minutes: 5 name: Tests with FsWatch (PHP ${{ matrix.php }}, OS ${{ matrix.os }}) runs-on: ${{ matrix.os }} diff --git a/src/Filesystem/ChangesListener/ChokidarChangesListener.php b/src/Filesystem/ChangesListener/ChokidarChangesListener.php index 5f580bd..5e8e786 100644 --- a/src/Filesystem/ChangesListener/ChokidarChangesListener.php +++ b/src/Filesystem/ChangesListener/ChokidarChangesListener.php @@ -56,7 +56,7 @@ public function stop(): void $this->process->stop(); } - if ($this->timer instanceof \React\EventLoop\TimerInterface) { + if ($this->timer instanceof TimerInterface) { $this->loop->cancelTimer($this->timer); } } diff --git a/src/ProcessRunner.php b/src/ProcessRunner.php index 2f87ebf..439f536 100644 --- a/src/ProcessRunner.php +++ b/src/ProcessRunner.php @@ -23,7 +23,7 @@ public function start(): void $this->screen->start($this->process->getCommand()); $this->screen->showSpinner($this->loop); - $this->process->start($this->loop); + $this->process->start(); $this->subscribeToProcessOutput(); } diff --git a/src/Screen/Screen.php b/src/Screen/Screen.php index 5defb90..64b19c8 100644 --- a/src/Screen/Screen.php +++ b/src/Screen/Screen.php @@ -13,6 +13,8 @@ final class Screen { + private ?\React\EventLoop\TimerInterface $spinTimer = null; + public function __construct(private readonly SymfonyStyle $output, private readonly SpinnerInterface $spinner) {} public function showOptions(WatchList $watchList): void @@ -84,11 +86,19 @@ public function plainOutput(string $data): void public function showSpinner(LoopInterface $loop): void { $this->spinner->begin(); - $loop->addPeriodicTimer($this->spinner->interval(), function (): void { + $this->spinTimer = $loop->addPeriodicTimer($this->spinner->interval(), function (): void { $this->spinner->spin(); }); } + public function stop(LoopInterface $loop): void + { + $this->spinner->end(); + if ($this->spinTimer instanceof \React\EventLoop\TimerInterface) { + $loop->cancelTimer($this->spinTimer); + } + } + private function message(string $text): string { return sprintf('[%s] %s', ConsoleApplication::NAME, $text); diff --git a/src/WatcherCommand.php b/src/WatcherCommand.php index 0a7e939..f0a08ee 100644 --- a/src/WatcherCommand.php +++ b/src/WatcherCommand.php @@ -4,7 +4,6 @@ namespace seregazhuk\PhpWatcher; -use AlecRabbit\Snake\Contracts\SpinnerInterface; use React\EventLoop\Loop; use React\EventLoop\LoopInterface; use seregazhuk\PhpWatcher\Config\Builder; @@ -72,7 +71,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int return Command::FAILURE; } - $this->addTerminationListeners($loop, $spinner, $filesystemListener); + $this->addTerminationListeners($loop, $screen, $filesystemListener); $screen->showOptions($config->watchList()); $screen->showFilesystemListener($filesystemListener); @@ -92,10 +91,10 @@ protected function execute(InputInterface $input, OutputInterface $output): int /** * When terminating the watcher, we need to manually restore the cursor after the spinner. */ - private function addTerminationListeners(LoopInterface $loop, SpinnerInterface $spinner, ChangesListenerInterface $changesListener): void + private function addTerminationListeners(LoopInterface $loop, Screen $screen, ChangesListenerInterface $changesListener): void { - $func = static function (int $signal) use ($spinner, $changesListener): never { - $spinner->end(); + $func = static function (int $signal) use ($screen, $changesListener, $loop): never { + $screen->stop($loop); $changesListener->stop(); exit($signal); }; From dc20323b67d99e16999e004b71b2ffdacbc6db04 Mon Sep 17 00:00:00 2001 From: Sergey Zhuk Date: Fri, 24 Oct 2025 11:58:47 +0300 Subject: [PATCH 20/59] feat: use fswatch --- .github/workflows/tests-chokidar.yml | 2 +- .github/workflows/tests-fswatch.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/tests-chokidar.yml b/.github/workflows/tests-chokidar.yml index f8c56b9..cf20ab0 100644 --- a/.github/workflows/tests-chokidar.yml +++ b/.github/workflows/tests-chokidar.yml @@ -30,7 +30,7 @@ jobs: php: [8.1, 8.2, 8.3, 8.4] env: - extensions: xdebug + extensions: xdebug, pcntl steps: - name: Check Out Code diff --git a/.github/workflows/tests-fswatch.yml b/.github/workflows/tests-fswatch.yml index ed3d988..6cb28a7 100644 --- a/.github/workflows/tests-fswatch.yml +++ b/.github/workflows/tests-fswatch.yml @@ -30,7 +30,7 @@ jobs: php: [8.1, 8.2, 8.3, 8.4] env: - extensions: xdebug + extensions: xdebug, pcntl steps: - name: Check Out Code From ad406f4316e486e3cb122ad914c6ae877628677e Mon Sep 17 00:00:00 2001 From: Sergey Zhuk Date: Fri, 24 Oct 2025 12:00:49 +0300 Subject: [PATCH 21/59] feat: use fswatch --- .github/workflows/tests-chokidar.yml | 4 ++-- .github/workflows/tests-fswatch.yml | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/tests-chokidar.yml b/.github/workflows/tests-chokidar.yml index cf20ab0..de25f7f 100644 --- a/.github/workflows/tests-chokidar.yml +++ b/.github/workflows/tests-chokidar.yml @@ -30,7 +30,7 @@ jobs: php: [8.1, 8.2, 8.3, 8.4] env: - extensions: xdebug, pcntl + extensions: xdebug steps: - name: Check Out Code @@ -46,7 +46,7 @@ jobs: uses: shivammathur/setup-php@v2 with: php-version: ${{ matrix.php }} - extensions: dom, sockets, grpc, curl ${{ matrix.extensions-suffix }} + extensions: dom, sockets, grpc, pcntl, curl ${{ matrix.extensions-suffix }} - name: Validate composer.json and composer.lock run: composer validate --strict diff --git a/.github/workflows/tests-fswatch.yml b/.github/workflows/tests-fswatch.yml index 6cb28a7..f9df1ef 100644 --- a/.github/workflows/tests-fswatch.yml +++ b/.github/workflows/tests-fswatch.yml @@ -30,7 +30,7 @@ jobs: php: [8.1, 8.2, 8.3, 8.4] env: - extensions: xdebug, pcntl + extensions: xdebug steps: - name: Check Out Code @@ -43,7 +43,7 @@ jobs: uses: shivammathur/setup-php@v2 with: php-version: ${{ matrix.php }} - extensions: dom, sockets, grpc, curl ${{ matrix.extensions-suffix }} + extensions: dom, sockets, grpc, pcntl, curl ${{ matrix.extensions-suffix }} - name: Validate composer.json and composer.lock run: composer validate --strict From dd2582f8bb73d1a5daf1ee785249bb170be67011 Mon Sep 17 00:00:00 2001 From: Sergey Zhuk Date: Fri, 24 Oct 2025 12:05:27 +0300 Subject: [PATCH 22/59] feat: use fswatch --- .github/workflows/tests-chokidar.yml | 2 +- .github/workflows/tests-fswatch.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/tests-chokidar.yml b/.github/workflows/tests-chokidar.yml index de25f7f..cc0ae92 100644 --- a/.github/workflows/tests-chokidar.yml +++ b/.github/workflows/tests-chokidar.yml @@ -46,7 +46,7 @@ jobs: uses: shivammathur/setup-php@v2 with: php-version: ${{ matrix.php }} - extensions: dom, sockets, grpc, pcntl, curl ${{ matrix.extensions-suffix }} + extensions: dom, sockets, pcntl - name: Validate composer.json and composer.lock run: composer validate --strict diff --git a/.github/workflows/tests-fswatch.yml b/.github/workflows/tests-fswatch.yml index f9df1ef..6ce9dd4 100644 --- a/.github/workflows/tests-fswatch.yml +++ b/.github/workflows/tests-fswatch.yml @@ -43,7 +43,7 @@ jobs: uses: shivammathur/setup-php@v2 with: php-version: ${{ matrix.php }} - extensions: dom, sockets, grpc, pcntl, curl ${{ matrix.extensions-suffix }} + extensions: dom, sockets, pcntl - name: Validate composer.json and composer.lock run: composer validate --strict From d03993ab1acea4ffd1a248e1c0c95d97a6e01e50 Mon Sep 17 00:00:00 2001 From: Sergey Zhuk Date: Fri, 24 Oct 2025 12:36:08 +0300 Subject: [PATCH 23/59] feat: use fswatch --- tests/Feature/SignalTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Feature/SignalTest.php b/tests/Feature/SignalTest.php index 31f7692..19023e3 100644 --- a/tests/Feature/SignalTest.php +++ b/tests/Feature/SignalTest.php @@ -13,7 +13,7 @@ final class SignalTest extends WatcherTestCase #[Test] public function it_sends_a_specified_signal_to_restart_the_app(): void { - if (! defined('SIGTERM')) { + if (! defined('SIGTERM') || !extension_loaded('pcntl')) { $this->markTestSkipped('SIGTERM is not defined'); } From dd2b5b3a674e1ca8cf07ac39f73c8c414681584f Mon Sep 17 00:00:00 2001 From: Sergey Zhuk Date: Fri, 24 Oct 2025 12:38:22 +0300 Subject: [PATCH 24/59] feat: use fswatch --- tests/Feature/ChokidarChangesListenerTest.php | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/Feature/ChokidarChangesListenerTest.php b/tests/Feature/ChokidarChangesListenerTest.php index 825666f..b5f3eee 100644 --- a/tests/Feature/ChokidarChangesListenerTest.php +++ b/tests/Feature/ChokidarChangesListenerTest.php @@ -33,5 +33,6 @@ public function it_emits_change_event_on_changes(): void delay(4); $this->assertTrue($eventWasEmitted, '"change" event should be emitted'); $listener->stop(); + $loop->stop(); } } From 6f6550702aedcf0cb2c2f0faa326a9959704af9f Mon Sep 17 00:00:00 2001 From: Sergey Zhuk Date: Fri, 24 Oct 2025 12:51:17 +0300 Subject: [PATCH 25/59] feat: use fswatch --- .github/workflows/tests-chokidar.yml | 3 +++ tests/Feature/ChokidarChangesListenerTest.php | 1 - tests/Feature/SignalTest.php | 2 +- 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/.github/workflows/tests-chokidar.yml b/.github/workflows/tests-chokidar.yml index cc0ae92..e726629 100644 --- a/.github/workflows/tests-chokidar.yml +++ b/.github/workflows/tests-chokidar.yml @@ -36,6 +36,9 @@ jobs: - name: Check Out Code uses: actions/checkout@v4 + - name: Remove fswatch + run: sudo apt-get uninstall fswatch + - name: Setup node uses: actions/setup-node@v6 diff --git a/tests/Feature/ChokidarChangesListenerTest.php b/tests/Feature/ChokidarChangesListenerTest.php index b5f3eee..825666f 100644 --- a/tests/Feature/ChokidarChangesListenerTest.php +++ b/tests/Feature/ChokidarChangesListenerTest.php @@ -33,6 +33,5 @@ public function it_emits_change_event_on_changes(): void delay(4); $this->assertTrue($eventWasEmitted, '"change" event should be emitted'); $listener->stop(); - $loop->stop(); } } diff --git a/tests/Feature/SignalTest.php b/tests/Feature/SignalTest.php index 19023e3..a7aefec 100644 --- a/tests/Feature/SignalTest.php +++ b/tests/Feature/SignalTest.php @@ -13,7 +13,7 @@ final class SignalTest extends WatcherTestCase #[Test] public function it_sends_a_specified_signal_to_restart_the_app(): void { - if (! defined('SIGTERM') || !extension_loaded('pcntl')) { + if (! defined('SIGTERM') || ! extension_loaded('pcntl')) { $this->markTestSkipped('SIGTERM is not defined'); } From 7d0a56ff3645747092a3a06421a15549fa16975b Mon Sep 17 00:00:00 2001 From: Sergey Zhuk Date: Fri, 24 Oct 2025 12:53:24 +0300 Subject: [PATCH 26/59] feat: use fswatch --- .github/workflows/tests-chokidar.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/tests-chokidar.yml b/.github/workflows/tests-chokidar.yml index e726629..2e9d84e 100644 --- a/.github/workflows/tests-chokidar.yml +++ b/.github/workflows/tests-chokidar.yml @@ -37,7 +37,7 @@ jobs: uses: actions/checkout@v4 - name: Remove fswatch - run: sudo apt-get uninstall fswatch + run: sudo apt-get remove fswatch - name: Setup node uses: actions/setup-node@v6 From ba26720a9ce3d391834eaf8a4c37d0311bbc9816 Mon Sep 17 00:00:00 2001 From: Sergey Zhuk Date: Fri, 24 Oct 2025 13:21:31 +0300 Subject: [PATCH 27/59] feat: use fswatch --- tests/Feature/FsWatchChangesListenerTest.php | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/tests/Feature/FsWatchChangesListenerTest.php b/tests/Feature/FsWatchChangesListenerTest.php index b997304..8f48f29 100644 --- a/tests/Feature/FsWatchChangesListenerTest.php +++ b/tests/Feature/FsWatchChangesListenerTest.php @@ -6,6 +6,7 @@ use PHPUnit\Framework\Attributes\Test; use PHPUnit\Framework\TestCase; +use React\ChildProcess\Process; use React\EventLoop\Loop; use seregazhuk\PhpWatcher\Config\WatchList; use seregazhuk\PhpWatcher\Filesystem\ChangesListener\FSWatchChangesListener; @@ -22,7 +23,11 @@ final class FsWatchChangesListenerTest extends TestCase #[Test] public function it_emits_change_event_on_changes(): void { - if (! FSWatchChangesListener::isAvailable()) { + $process = new Process("which fswatch"); + $process->start(); + delay(0.1); + + if ($process->getExitCode() !== 0) { $this->markTestSkipped('fswatch is not available'); } From fb5088e6f60260d1d1ef1463d62a49d96162aab8 Mon Sep 17 00:00:00 2001 From: Sergey Zhuk Date: Fri, 24 Oct 2025 13:35:54 +0300 Subject: [PATCH 28/59] feat: use fswatch --- composer.json | 2 +- tests/Feature/FsWatchChangesListenerTest.php | 7 +------ 2 files changed, 2 insertions(+), 7 deletions(-) diff --git a/composer.json b/composer.json index f08ba3a..ebb8baa 100644 --- a/composer.json +++ b/composer.json @@ -31,7 +31,7 @@ "react/child-process": "^0.6.1", "react/event-loop": "^1.1", "react/stream": "^1.0.0", - "seregazhuk/reactphp-fswatch": "^1.1.0", + "seregazhuk/reactphp-fswatch": "^1.1.1", "symfony/console": "^6.0", "symfony/finder": "^6.0", "symfony/process": "^6.0", diff --git a/tests/Feature/FsWatchChangesListenerTest.php b/tests/Feature/FsWatchChangesListenerTest.php index 8f48f29..b997304 100644 --- a/tests/Feature/FsWatchChangesListenerTest.php +++ b/tests/Feature/FsWatchChangesListenerTest.php @@ -6,7 +6,6 @@ use PHPUnit\Framework\Attributes\Test; use PHPUnit\Framework\TestCase; -use React\ChildProcess\Process; use React\EventLoop\Loop; use seregazhuk\PhpWatcher\Config\WatchList; use seregazhuk\PhpWatcher\Filesystem\ChangesListener\FSWatchChangesListener; @@ -23,11 +22,7 @@ final class FsWatchChangesListenerTest extends TestCase #[Test] public function it_emits_change_event_on_changes(): void { - $process = new Process("which fswatch"); - $process->start(); - delay(0.1); - - if ($process->getExitCode() !== 0) { + if (! FSWatchChangesListener::isAvailable()) { $this->markTestSkipped('fswatch is not available'); } From 664da06882021217f476516aeb53d49de251f81b Mon Sep 17 00:00:00 2001 From: Sergey Zhuk Date: Fri, 24 Oct 2025 13:41:22 +0300 Subject: [PATCH 29/59] feat: use fswatch --- tests/Feature/ChokidarChangesListenerTest.php | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/tests/Feature/ChokidarChangesListenerTest.php b/tests/Feature/ChokidarChangesListenerTest.php index 825666f..30c32eb 100644 --- a/tests/Feature/ChokidarChangesListenerTest.php +++ b/tests/Feature/ChokidarChangesListenerTest.php @@ -12,6 +12,8 @@ use seregazhuk\PhpWatcher\Tests\Feature\Helper\Filesystem; use seregazhuk\PhpWatcher\Tests\Feature\Helper\WithFilesystem; +use Symfony\Component\Process\Process; + use function React\Async\delay; final class ChokidarChangesListenerTest extends TestCase @@ -21,6 +23,12 @@ final class ChokidarChangesListenerTest extends TestCase #[Test] public function it_emits_change_event_on_changes(): void { + $process = new Process(['node', '-v']); + $process->run(); + if($process->isSuccessful()) { + $this->markTestSkipped('nodejs is not available'); + } + $loop = Loop::get(); $listener = new ChokidarChangesListener($loop); $listener->start(new WatchList([Filesystem::fixturesDir()])); From 44e6d8f04bc3eb119486cb73fb2f9e4189fc9967 Mon Sep 17 00:00:00 2001 From: Sergey Zhuk Date: Fri, 24 Oct 2025 13:41:30 +0300 Subject: [PATCH 30/59] feat: use fswatch --- tests/Feature/ChokidarChangesListenerTest.php | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tests/Feature/ChokidarChangesListenerTest.php b/tests/Feature/ChokidarChangesListenerTest.php index 30c32eb..08dc218 100644 --- a/tests/Feature/ChokidarChangesListenerTest.php +++ b/tests/Feature/ChokidarChangesListenerTest.php @@ -11,7 +11,6 @@ use seregazhuk\PhpWatcher\Filesystem\ChangesListener\ChokidarChangesListener; use seregazhuk\PhpWatcher\Tests\Feature\Helper\Filesystem; use seregazhuk\PhpWatcher\Tests\Feature\Helper\WithFilesystem; - use Symfony\Component\Process\Process; use function React\Async\delay; @@ -25,7 +24,7 @@ public function it_emits_change_event_on_changes(): void { $process = new Process(['node', '-v']); $process->run(); - if($process->isSuccessful()) { + if ($process->isSuccessful()) { $this->markTestSkipped('nodejs is not available'); } From 058705fdb4f3ed58762b84b1b2f6bb72ac7a7a3a Mon Sep 17 00:00:00 2001 From: Sergey Zhuk Date: Fri, 24 Oct 2025 13:46:41 +0300 Subject: [PATCH 31/59] feat: use fswatch --- tests/Feature/ChokidarChangesListenerTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Feature/ChokidarChangesListenerTest.php b/tests/Feature/ChokidarChangesListenerTest.php index 08dc218..37b1002 100644 --- a/tests/Feature/ChokidarChangesListenerTest.php +++ b/tests/Feature/ChokidarChangesListenerTest.php @@ -24,7 +24,7 @@ public function it_emits_change_event_on_changes(): void { $process = new Process(['node', '-v']); $process->run(); - if ($process->isSuccessful()) { + if (! $process->isSuccessful()) { $this->markTestSkipped('nodejs is not available'); } From 9e426107114ac95ef6508e82fb41c7c3a6fe3702 Mon Sep 17 00:00:00 2001 From: Sergey Zhuk Date: Fri, 24 Oct 2025 13:48:50 +0300 Subject: [PATCH 32/59] feat: use fswatch --- tests/Feature/ChokidarChangesListenerTest.php | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/tests/Feature/ChokidarChangesListenerTest.php b/tests/Feature/ChokidarChangesListenerTest.php index 37b1002..0ddcc1b 100644 --- a/tests/Feature/ChokidarChangesListenerTest.php +++ b/tests/Feature/ChokidarChangesListenerTest.php @@ -9,6 +9,7 @@ use React\EventLoop\Loop; use seregazhuk\PhpWatcher\Config\WatchList; use seregazhuk\PhpWatcher\Filesystem\ChangesListener\ChokidarChangesListener; +use seregazhuk\PhpWatcher\SystemRequirements\SystemRequirementsChecker; use seregazhuk\PhpWatcher\Tests\Feature\Helper\Filesystem; use seregazhuk\PhpWatcher\Tests\Feature\Helper\WithFilesystem; use Symfony\Component\Process\Process; @@ -22,10 +23,8 @@ final class ChokidarChangesListenerTest extends TestCase #[Test] public function it_emits_change_event_on_changes(): void { - $process = new Process(['node', '-v']); - $process->run(); - if (! $process->isSuccessful()) { - $this->markTestSkipped('nodejs is not available'); + if (! SystemRequirementsChecker::isChokidarInstalled()) { + $this->markTestSkipped('chokidar is not available'); } $loop = Loop::get(); From f6681f89657c7c52679b720084b59e52c70d47d2 Mon Sep 17 00:00:00 2001 From: Sergey Zhuk Date: Fri, 24 Oct 2025 13:52:07 +0300 Subject: [PATCH 33/59] feat: use fswatch --- tests/Feature/ChokidarChangesListenerTest.php | 1 - tests/Feature/FsWatchChangesListenerTest.php | 7 +++++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/tests/Feature/ChokidarChangesListenerTest.php b/tests/Feature/ChokidarChangesListenerTest.php index 0ddcc1b..0d0fcc1 100644 --- a/tests/Feature/ChokidarChangesListenerTest.php +++ b/tests/Feature/ChokidarChangesListenerTest.php @@ -12,7 +12,6 @@ use seregazhuk\PhpWatcher\SystemRequirements\SystemRequirementsChecker; use seregazhuk\PhpWatcher\Tests\Feature\Helper\Filesystem; use seregazhuk\PhpWatcher\Tests\Feature\Helper\WithFilesystem; -use Symfony\Component\Process\Process; use function React\Async\delay; diff --git a/tests/Feature/FsWatchChangesListenerTest.php b/tests/Feature/FsWatchChangesListenerTest.php index b997304..a748c2c 100644 --- a/tests/Feature/FsWatchChangesListenerTest.php +++ b/tests/Feature/FsWatchChangesListenerTest.php @@ -32,10 +32,13 @@ public function it_emits_change_event_on_changes(): void $listener = new FSWatchChangesListener; $listener->start(new WatchList([Filesystem::fixturesDir()])); - $listener->onChange(function (): void { - $this->assertTrue(true); + $eventWasEmitted = false; + $listener->onChange(function () use (&$eventWasEmitted): void { + $eventWasEmitted = true; }); delay(4); + $this->assertTrue($eventWasEmitted, '"change" event should be emitted'); + $listener->stop(); $loop->stop(); } } From ce2a0a344c3bc0a25466bffaa593ea7564f4aac2 Mon Sep 17 00:00:00 2001 From: Sergey Zhuk Date: Fri, 24 Oct 2025 15:00:09 +0300 Subject: [PATCH 34/59] feat: use fswatch --- tests/Feature/FsWatchChangesListenerTest.php | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/tests/Feature/FsWatchChangesListenerTest.php b/tests/Feature/FsWatchChangesListenerTest.php index a748c2c..083b7dc 100644 --- a/tests/Feature/FsWatchChangesListenerTest.php +++ b/tests/Feature/FsWatchChangesListenerTest.php @@ -7,6 +7,7 @@ use PHPUnit\Framework\Attributes\Test; use PHPUnit\Framework\TestCase; use React\EventLoop\Loop; +use React\EventLoop\Timer\Timer; use seregazhuk\PhpWatcher\Config\WatchList; use seregazhuk\PhpWatcher\Filesystem\ChangesListener\FSWatchChangesListener; use seregazhuk\PhpWatcher\Tests\Feature\Helper\Filesystem; @@ -27,18 +28,18 @@ public function it_emits_change_event_on_changes(): void } $loop = Loop::get(); - $loop->addTimer(1, async(Filesystem::createHelloWorldPHPFile(...))); - $listener = new FSWatchChangesListener; - $listener->start(new WatchList([Filesystem::fixturesDir()])); $eventWasEmitted = false; $listener->onChange(function () use (&$eventWasEmitted): void { $eventWasEmitted = true; }); - delay(4); + $loop->addTimer(1, Filesystem::createHelloWorldPHPFile(...)); + + $listener->start(new WatchList([Filesystem::fixturesDir()])); + $loop->addTimer(3, fn() => $loop->stop()); + + delay(2); $this->assertTrue($eventWasEmitted, '"change" event should be emitted'); - $listener->stop(); - $loop->stop(); } } From 0e4c12215b0320be801c262495395ed90e69c10c Mon Sep 17 00:00:00 2001 From: Sergey Zhuk Date: Fri, 24 Oct 2025 15:00:21 +0300 Subject: [PATCH 35/59] feat: use fswatch --- tests/Feature/FsWatchChangesListenerTest.php | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/tests/Feature/FsWatchChangesListenerTest.php b/tests/Feature/FsWatchChangesListenerTest.php index 083b7dc..19f85d9 100644 --- a/tests/Feature/FsWatchChangesListenerTest.php +++ b/tests/Feature/FsWatchChangesListenerTest.php @@ -7,13 +7,11 @@ use PHPUnit\Framework\Attributes\Test; use PHPUnit\Framework\TestCase; use React\EventLoop\Loop; -use React\EventLoop\Timer\Timer; use seregazhuk\PhpWatcher\Config\WatchList; use seregazhuk\PhpWatcher\Filesystem\ChangesListener\FSWatchChangesListener; use seregazhuk\PhpWatcher\Tests\Feature\Helper\Filesystem; use seregazhuk\PhpWatcher\Tests\Feature\Helper\WithFilesystem; -use function React\Async\async; use function React\Async\delay; final class FsWatchChangesListenerTest extends TestCase @@ -37,7 +35,7 @@ public function it_emits_change_event_on_changes(): void $loop->addTimer(1, Filesystem::createHelloWorldPHPFile(...)); $listener->start(new WatchList([Filesystem::fixturesDir()])); - $loop->addTimer(3, fn() => $loop->stop()); + $loop->addTimer(3, fn () => $loop->stop()); delay(2); $this->assertTrue($eventWasEmitted, '"change" event should be emitted'); From 80c5127aedc3b9922804dfd21b6d35f74652cc4d Mon Sep 17 00:00:00 2001 From: Sergey Zhuk Date: Fri, 24 Oct 2025 15:05:08 +0300 Subject: [PATCH 36/59] feat: use fswatch --- tests/Feature/FsWatchChangesListenerTest.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/Feature/FsWatchChangesListenerTest.php b/tests/Feature/FsWatchChangesListenerTest.php index 19f85d9..00bf85d 100644 --- a/tests/Feature/FsWatchChangesListenerTest.php +++ b/tests/Feature/FsWatchChangesListenerTest.php @@ -35,9 +35,9 @@ public function it_emits_change_event_on_changes(): void $loop->addTimer(1, Filesystem::createHelloWorldPHPFile(...)); $listener->start(new WatchList([Filesystem::fixturesDir()])); - $loop->addTimer(3, fn () => $loop->stop()); + $loop->addTimer(5, fn () => $loop->stop()); - delay(2); + delay(4); $this->assertTrue($eventWasEmitted, '"change" event should be emitted'); } } From e245a6504c919ec0a206b6bbd6d7cf35af2b3f41 Mon Sep 17 00:00:00 2001 From: Sergey Zhuk Date: Fri, 24 Oct 2025 15:18:03 +0300 Subject: [PATCH 37/59] feat: use fswatch --- .../ChangesListener/FSWatchChangesListener.php | 1 - tests/Feature/FsWatchChangesListenerTest.php | 14 ++++++++++---- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/src/Filesystem/ChangesListener/FSWatchChangesListener.php b/src/Filesystem/ChangesListener/FSWatchChangesListener.php index 5be4701..edd8794 100644 --- a/src/Filesystem/ChangesListener/FSWatchChangesListener.php +++ b/src/Filesystem/ChangesListener/FSWatchChangesListener.php @@ -20,7 +20,6 @@ public static function isAvailable(): bool public function start(WatchList $watchList): void { $this->fsWatch = new FsWatch($this->makeOptions($watchList)); - $this->fsWatch->run(); $this->fsWatch->onChange(fn () => $this->emit('change')); } diff --git a/tests/Feature/FsWatchChangesListenerTest.php b/tests/Feature/FsWatchChangesListenerTest.php index 00bf85d..1976ca4 100644 --- a/tests/Feature/FsWatchChangesListenerTest.php +++ b/tests/Feature/FsWatchChangesListenerTest.php @@ -12,6 +12,8 @@ use seregazhuk\PhpWatcher\Tests\Feature\Helper\Filesystem; use seregazhuk\PhpWatcher\Tests\Feature\Helper\WithFilesystem; +use Symfony\Component\Process\Process; + use function React\Async\delay; final class FsWatchChangesListenerTest extends TestCase @@ -34,10 +36,14 @@ public function it_emits_change_event_on_changes(): void }); $loop->addTimer(1, Filesystem::createHelloWorldPHPFile(...)); - $listener->start(new WatchList([Filesystem::fixturesDir()])); - $loop->addTimer(5, fn () => $loop->stop()); + $listener->start(new WatchList([__DIR__ . '/../../' .Filesystem::fixturesDir()])); + $loop->addTimer(4, fn () => $loop->stop()); + $fswatch = new Process(['fswatch', '-xrn', '../../tests/fixtures/', '-e', '".*"', '-i', '.php$', '-I']); + $fswatch->start(); + sleep(1); + $fswatch->stop(); + $this->assertSame('test', $fswatch->getOutput() . ' ' . $fswatch->getErrorOutput()); - delay(4); - $this->assertTrue($eventWasEmitted, '"change" event should be emitted'); +// $this->assertTrue($eventWasEmitted, '"change" event should be emitted'); } } From e2ea7ab3ed2bc1cb0d93aa73d4cc21c9c5ab3a7c Mon Sep 17 00:00:00 2001 From: Sergey Zhuk Date: Fri, 24 Oct 2025 15:24:50 +0300 Subject: [PATCH 38/59] feat: use fswatch --- tests/Feature/FsWatchChangesListenerTest.php | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/tests/Feature/FsWatchChangesListenerTest.php b/tests/Feature/FsWatchChangesListenerTest.php index 1976ca4..e5c3c18 100644 --- a/tests/Feature/FsWatchChangesListenerTest.php +++ b/tests/Feature/FsWatchChangesListenerTest.php @@ -37,13 +37,8 @@ public function it_emits_change_event_on_changes(): void $loop->addTimer(1, Filesystem::createHelloWorldPHPFile(...)); $listener->start(new WatchList([__DIR__ . '/../../' .Filesystem::fixturesDir()])); + delay(3); $loop->addTimer(4, fn () => $loop->stop()); - $fswatch = new Process(['fswatch', '-xrn', '../../tests/fixtures/', '-e', '".*"', '-i', '.php$', '-I']); - $fswatch->start(); - sleep(1); - $fswatch->stop(); - $this->assertSame('test', $fswatch->getOutput() . ' ' . $fswatch->getErrorOutput()); - -// $this->assertTrue($eventWasEmitted, '"change" event should be emitted'); + $this->assertTrue($eventWasEmitted, '"change" event should be emitted'); } } From 2a53dbf686d4a9c35e85cd984fdf007f3635cccf Mon Sep 17 00:00:00 2001 From: Sergey Zhuk Date: Fri, 24 Oct 2025 15:27:49 +0300 Subject: [PATCH 39/59] feat: use fswatch --- tests/Feature/FsWatchChangesListenerTest.php | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/tests/Feature/FsWatchChangesListenerTest.php b/tests/Feature/FsWatchChangesListenerTest.php index e5c3c18..266ea89 100644 --- a/tests/Feature/FsWatchChangesListenerTest.php +++ b/tests/Feature/FsWatchChangesListenerTest.php @@ -35,10 +35,11 @@ public function it_emits_change_event_on_changes(): void $eventWasEmitted = true; }); $loop->addTimer(1, Filesystem::createHelloWorldPHPFile(...)); + $loop->addTimer(3, fn () => $loop->stop()); + + $listener->start(new WatchList([Filesystem::fixturesDir()])); + delay(2); - $listener->start(new WatchList([__DIR__ . '/../../' .Filesystem::fixturesDir()])); - delay(3); - $loop->addTimer(4, fn () => $loop->stop()); $this->assertTrue($eventWasEmitted, '"change" event should be emitted'); } } From 95be314d0dd968d82f9025cd6022fdc8067baa11 Mon Sep 17 00:00:00 2001 From: Sergey Zhuk Date: Fri, 24 Oct 2025 15:33:23 +0300 Subject: [PATCH 40/59] feat: use fswatch --- tests/Feature/FsWatchChangesListenerTest.php | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/tests/Feature/FsWatchChangesListenerTest.php b/tests/Feature/FsWatchChangesListenerTest.php index 266ea89..a4c0c85 100644 --- a/tests/Feature/FsWatchChangesListenerTest.php +++ b/tests/Feature/FsWatchChangesListenerTest.php @@ -23,9 +23,7 @@ final class FsWatchChangesListenerTest extends TestCase #[Test] public function it_emits_change_event_on_changes(): void { - if (! FSWatchChangesListener::isAvailable()) { - $this->markTestSkipped('fswatch is not available'); - } + $this->markTestSkipped('fswatch is not available'); $loop = Loop::get(); $listener = new FSWatchChangesListener; From 5a18375929eedefc933bc949841a573916869762 Mon Sep 17 00:00:00 2001 From: Sergey Zhuk Date: Fri, 24 Oct 2025 15:41:06 +0300 Subject: [PATCH 41/59] feat: use fswatch --- .github/workflows/tests-fswatch.yml | 2 +- phpunit.xml.dist | 4 ++++ tests/Feature/FsWatchChangesListenerTest.php | 6 +++--- 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/.github/workflows/tests-fswatch.yml b/.github/workflows/tests-fswatch.yml index 6ce9dd4..b70ea6e 100644 --- a/.github/workflows/tests-fswatch.yml +++ b/.github/workflows/tests-fswatch.yml @@ -61,4 +61,4 @@ jobs: - name: Run tests with Phpunit run: | - XDEBUG_MODE=coverage php vendor/bin/phpunit + XDEBUG_MODE=coverage php vendor/bin/phpunit --testsuite=ExceptFsWatchListener diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 23319e3..554bd7a 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -12,6 +12,10 @@ tests + + ./tests + ./tests/Feature/FsWatchChangesListenerTest.php + diff --git a/tests/Feature/FsWatchChangesListenerTest.php b/tests/Feature/FsWatchChangesListenerTest.php index a4c0c85..0b7a60e 100644 --- a/tests/Feature/FsWatchChangesListenerTest.php +++ b/tests/Feature/FsWatchChangesListenerTest.php @@ -12,8 +12,6 @@ use seregazhuk\PhpWatcher\Tests\Feature\Helper\Filesystem; use seregazhuk\PhpWatcher\Tests\Feature\Helper\WithFilesystem; -use Symfony\Component\Process\Process; - use function React\Async\delay; final class FsWatchChangesListenerTest extends TestCase @@ -23,7 +21,9 @@ final class FsWatchChangesListenerTest extends TestCase #[Test] public function it_emits_change_event_on_changes(): void { - $this->markTestSkipped('fswatch is not available'); + if (! FSWatchChangesListener::isAvailable()) { + $this->markTestSkipped('fswatch is not available'); + } $loop = Loop::get(); $listener = new FSWatchChangesListener; From 84ef8fc962574f2182cea5b00e99564431c3e3dd Mon Sep 17 00:00:00 2001 From: Sergey Zhuk Date: Fri, 24 Oct 2025 16:29:08 +0300 Subject: [PATCH 42/59] feat: use fswatch --- server.php | 5 ----- src/ProcessRunner.php | 6 +++++- 2 files changed, 5 insertions(+), 6 deletions(-) delete mode 100644 server.php diff --git a/server.php b/server.php deleted file mode 100644 index f83511d..0000000 --- a/server.php +++ /dev/null @@ -1,5 +0,0 @@ -screen->start($this->process->getCommand()); $this->screen->showSpinner($this->loop); - $this->process->start(); + if (!$this->process->isRunning()) { + $this->process->start(); + } $this->subscribeToProcessOutput(); } From 3438367d5be1420a5bd4677b6c612d8e325522c5 Mon Sep 17 00:00:00 2001 From: Sergey Zhuk Date: Fri, 24 Oct 2025 16:38:42 +0300 Subject: [PATCH 43/59] feat: use fswatch --- tests/Feature/IgnoreFilesTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Feature/IgnoreFilesTest.php b/tests/Feature/IgnoreFilesTest.php index 832deca..e549652 100644 --- a/tests/Feature/IgnoreFilesTest.php +++ b/tests/Feature/IgnoreFilesTest.php @@ -31,6 +31,6 @@ public function it_doesnt_reload_when_ignored_directories_change(): void Filesystem::changeFileContentsWith($fileToWatch, 'wait(); - $this->assertOutputDoesntContain('restarting due to changes...'); + $this->assertOutputDoesntContain('restarting due to changes'); } } From 191d39da3a2dc2a5a41901f8062cbf272bf62519 Mon Sep 17 00:00:00 2001 From: Sergey Zhuk Date: Fri, 24 Oct 2025 16:41:57 +0300 Subject: [PATCH 44/59] feat: use fswatch --- tests/Feature/IgnoreFilesTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Feature/IgnoreFilesTest.php b/tests/Feature/IgnoreFilesTest.php index e549652..83bb555 100644 --- a/tests/Feature/IgnoreFilesTest.php +++ b/tests/Feature/IgnoreFilesTest.php @@ -19,7 +19,7 @@ public function it_doesnt_reload_when_ignored_files_change(): void Filesystem::changeFileContentsWith($fileToWatch, 'wait(); - $this->assertOutputDoesntContain('restarting due to changes...'); + $this->assertOutputDoesntContain('restarting due to changes'); } #[Test] From 5cc4e9dcb1deac46a5c10943f3a6fb3ce8834c58 Mon Sep 17 00:00:00 2001 From: Sergey Zhuk Date: Fri, 24 Oct 2025 16:50:03 +0300 Subject: [PATCH 45/59] feat: use fswatch --- src/Filesystem/ChangesListener/FSWatchChangesListener.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Filesystem/ChangesListener/FSWatchChangesListener.php b/src/Filesystem/ChangesListener/FSWatchChangesListener.php index edd8794..32a8edc 100644 --- a/src/Filesystem/ChangesListener/FSWatchChangesListener.php +++ b/src/Filesystem/ChangesListener/FSWatchChangesListener.php @@ -72,7 +72,7 @@ private function makeIncludeOptions(WatchList $watchList): array } $regexpWithExtensions = array_map( - static fn ($extension) => str_replace('*.', '.', $extension).'$', + static fn ($extension) => '"'. str_replace('*.', '.', $extension).'$"', $watchList->getFileExtensions() ); $options[] = '-i '.implode(' ', $regexpWithExtensions); From 81c7158e4bf1fa88e4a2c3ad35c26964882ddda8 Mon Sep 17 00:00:00 2001 From: Sergey Zhuk Date: Fri, 24 Oct 2025 17:20:51 +0300 Subject: [PATCH 46/59] feat: use fswatch --- .../ChangesListener/FSWatchChangesListener.php | 17 +++++++++-------- src/Screen/Screen.php | 11 ++++++++--- src/WatcherCommand.php | 1 + 3 files changed, 18 insertions(+), 11 deletions(-) diff --git a/src/Filesystem/ChangesListener/FSWatchChangesListener.php b/src/Filesystem/ChangesListener/FSWatchChangesListener.php index 32a8edc..4d538a9 100644 --- a/src/Filesystem/ChangesListener/FSWatchChangesListener.php +++ b/src/Filesystem/ChangesListener/FSWatchChangesListener.php @@ -6,6 +6,7 @@ use Evenement\EventEmitter; use seregazhuk\PhpWatcher\Config\WatchList; +use Seregazhuk\ReactFsWatch\Change; use Seregazhuk\ReactFsWatch\FsWatch; final class FSWatchChangesListener extends EventEmitter implements ChangesListenerInterface @@ -21,7 +22,12 @@ public function start(WatchList $watchList): void { $this->fsWatch = new FsWatch($this->makeOptions($watchList)); $this->fsWatch->run(); - $this->fsWatch->onChange(fn () => $this->emit('change')); + $this->fsWatch->onChange(function (Change $fsWatchChange) use ($watchList) { + $isIgnored = in_array($fsWatchChange->file(), $watchList->getIgnored(), true); + if (!$isIgnored) { + $this->emit('change'); + } + }); } public function onChange(callable $callback): void @@ -45,11 +51,6 @@ private function makeOptions(WatchList $watchList): string $options[] = implode(' ', $watchList->getPaths()); } - // then we ignore - if ($watchList->getIgnored() !== []) { - $options[] = '-e '.implode(' ', $watchList->getIgnored()); - } - // then include if ($watchList->getFileExtensions() !== []) { $options = array_merge($options, $this->makeIncludeOptions($watchList)); @@ -72,10 +73,10 @@ private function makeIncludeOptions(WatchList $watchList): array } $regexpWithExtensions = array_map( - static fn ($extension) => '"'. str_replace('*.', '.', $extension).'$"', + static fn($extension) => '"' . str_replace('*.', '.', $extension) . '$"', $watchList->getFileExtensions() ); - $options[] = '-i '.implode(' ', $regexpWithExtensions); + $options[] = '-i ' . implode(' ', $regexpWithExtensions); return $options; } diff --git a/src/Screen/Screen.php b/src/Screen/Screen.php index 64b19c8..daed3b9 100644 --- a/src/Screen/Screen.php +++ b/src/Screen/Screen.php @@ -6,6 +6,7 @@ use AlecRabbit\Snake\Contracts\SpinnerInterface; use React\EventLoop\LoopInterface; +use React\EventLoop\TimerInterface; use seregazhuk\PhpWatcher\Config\WatchList; use seregazhuk\PhpWatcher\ConsoleApplication; use seregazhuk\PhpWatcher\Filesystem\ChangesListener\ChangesListenerInterface; @@ -13,7 +14,7 @@ final class Screen { - private ?\React\EventLoop\TimerInterface $spinTimer = null; + private ?TimerInterface $spinTimer = null; public function __construct(private readonly SymfonyStyle $output, private readonly SpinnerInterface $spinner) {} @@ -69,8 +70,12 @@ public function restarting(): void $this->info('restarting due to changes...'); } - public function processExit(int $exitCode): void + public function processExit(?int $exitCode): void { + if ($exitCode === null) { + $this->info('Stopping watcher...'); + return; + } if ($exitCode === 0) { $this->info('clean exit - waiting for changes before restart'); } else { @@ -94,7 +99,7 @@ public function showSpinner(LoopInterface $loop): void public function stop(LoopInterface $loop): void { $this->spinner->end(); - if ($this->spinTimer instanceof \React\EventLoop\TimerInterface) { + if ($this->spinTimer instanceof TimerInterface) { $loop->cancelTimer($this->spinTimer); } } diff --git a/src/WatcherCommand.php b/src/WatcherCommand.php index f0a08ee..76394e8 100644 --- a/src/WatcherCommand.php +++ b/src/WatcherCommand.php @@ -96,6 +96,7 @@ private function addTerminationListeners(LoopInterface $loop, Screen $screen, Ch $func = static function (int $signal) use ($screen, $changesListener, $loop): never { $screen->stop($loop); $changesListener->stop(); + $loop->stop(); exit($signal); }; From cf6fa440cf84b92c0882fef626c93abf39c022f8 Mon Sep 17 00:00:00 2001 From: Sergey Zhuk Date: Fri, 24 Oct 2025 17:49:32 +0300 Subject: [PATCH 47/59] feat: use fswatch --- .../FSWatchChangesListener.php | 23 +++++++++++++++---- src/ProcessRunner.php | 4 +--- src/Screen/Screen.php | 1 + 3 files changed, 20 insertions(+), 8 deletions(-) diff --git a/src/Filesystem/ChangesListener/FSWatchChangesListener.php b/src/Filesystem/ChangesListener/FSWatchChangesListener.php index 4d538a9..28456fc 100644 --- a/src/Filesystem/ChangesListener/FSWatchChangesListener.php +++ b/src/Filesystem/ChangesListener/FSWatchChangesListener.php @@ -20,11 +20,24 @@ public static function isAvailable(): bool public function start(WatchList $watchList): void { + $checkPathIsIgnored = function (string $path) use ($watchList): bool { + foreach ($watchList->getIgnored() as $ignoredPath) { + if (realpath($ignoredPath) === false && basename($path) === $ignoredPath) { + return true; + } + if (realpath($ignoredPath) !== false && $path === $ignoredPath) { + return true; + } + } + + return false; + }; + $this->fsWatch = new FsWatch($this->makeOptions($watchList)); $this->fsWatch->run(); - $this->fsWatch->onChange(function (Change $fsWatchChange) use ($watchList) { - $isIgnored = in_array($fsWatchChange->file(), $watchList->getIgnored(), true); - if (!$isIgnored) { + $this->fsWatch->onChange(function (Change $fsWatchChange) use ($checkPathIsIgnored): void { + $isIgnored = $checkPathIsIgnored($fsWatchChange->file()); + if (! $isIgnored) { $this->emit('change'); } }); @@ -73,10 +86,10 @@ private function makeIncludeOptions(WatchList $watchList): array } $regexpWithExtensions = array_map( - static fn($extension) => '"' . str_replace('*.', '.', $extension) . '$"', + static fn ($extension): string => '"'.str_replace('*.', '.', $extension).'$"', $watchList->getFileExtensions() ); - $options[] = '-i ' . implode(' ', $regexpWithExtensions); + $options[] = '-i '.implode(' ', $regexpWithExtensions); return $options; } diff --git a/src/ProcessRunner.php b/src/ProcessRunner.php index f6e3f85..d5f2c57 100644 --- a/src/ProcessRunner.php +++ b/src/ProcessRunner.php @@ -9,8 +9,6 @@ use RuntimeException; use seregazhuk\PhpWatcher\Screen\Screen; -use function React\Async\delay; - final class ProcessRunner { private readonly ReactPHPProcess $process; @@ -25,7 +23,7 @@ public function start(): void $this->screen->start($this->process->getCommand()); $this->screen->showSpinner($this->loop); - if (!$this->process->isRunning()) { + if (! $this->process->isRunning()) { $this->process->start(); } $this->subscribeToProcessOutput(); diff --git a/src/Screen/Screen.php b/src/Screen/Screen.php index daed3b9..c6b83a6 100644 --- a/src/Screen/Screen.php +++ b/src/Screen/Screen.php @@ -74,6 +74,7 @@ public function processExit(?int $exitCode): void { if ($exitCode === null) { $this->info('Stopping watcher...'); + return; } if ($exitCode === 0) { From e0d036156a48f40929e5258a3901911338fa49ae Mon Sep 17 00:00:00 2001 From: Sergey Zhuk Date: Fri, 24 Oct 2025 17:56:29 +0300 Subject: [PATCH 48/59] feat: use fswatch --- src/Filesystem/ChangesListener/FSWatchChangesListener.php | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/Filesystem/ChangesListener/FSWatchChangesListener.php b/src/Filesystem/ChangesListener/FSWatchChangesListener.php index 28456fc..8262f84 100644 --- a/src/Filesystem/ChangesListener/FSWatchChangesListener.php +++ b/src/Filesystem/ChangesListener/FSWatchChangesListener.php @@ -79,11 +79,8 @@ private function makeOptions(WatchList $watchList): string */ private function makeIncludeOptions(WatchList $watchList): array { - $options = []; // Before including we need to ignore everything - if ($watchList->getIgnored() === []) { - $options[] = '-e ".*"'; - } + $options[] = '-e ".*"'; $regexpWithExtensions = array_map( static fn ($extension): string => '"'.str_replace('*.', '.', $extension).'$"', From baae2040faf9ee2be4f20f7bd791b0ca3decbd72 Mon Sep 17 00:00:00 2001 From: Sergey Zhuk Date: Fri, 24 Oct 2025 18:05:19 +0300 Subject: [PATCH 49/59] feat: use fswatch --- src/Filesystem/ChangesListener/FSWatchChangesListener.php | 2 ++ tests/Feature/SignalTest.php | 1 + 2 files changed, 3 insertions(+) diff --git a/src/Filesystem/ChangesListener/FSWatchChangesListener.php b/src/Filesystem/ChangesListener/FSWatchChangesListener.php index 8262f84..24f2a62 100644 --- a/src/Filesystem/ChangesListener/FSWatchChangesListener.php +++ b/src/Filesystem/ChangesListener/FSWatchChangesListener.php @@ -20,6 +20,8 @@ public static function isAvailable(): bool public function start(WatchList $watchList): void { + // We need to manually check ignored paths + // https://stackoverflow.com/questions/34713278/fswatch-to-watch-only-a-certain-file-extension $checkPathIsIgnored = function (string $path) use ($watchList): bool { foreach ($watchList->getIgnored() as $ignoredPath) { if (realpath($ignoredPath) === false && basename($path) === $ignoredPath) { diff --git a/tests/Feature/SignalTest.php b/tests/Feature/SignalTest.php index a7aefec..9c97482 100644 --- a/tests/Feature/SignalTest.php +++ b/tests/Feature/SignalTest.php @@ -23,6 +23,7 @@ public function it_sends_a_specified_signal_to_restart_the_app(): void Filesystem::createHelloWorldPHPFile(); $this->wait(); + $this->wait(); $this->assertOutputContains(SIGTERM.' signal was received'); } From 611dc93a653858426cdcb5660696db14772ce437 Mon Sep 17 00:00:00 2001 From: Sergey Zhuk Date: Fri, 24 Oct 2025 18:15:16 +0300 Subject: [PATCH 50/59] feat: use fswatch --- tests/Feature/Helper/Filesystem.php | 2 +- tests/Feature/SignalTest.php | 5 ++--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/tests/Feature/Helper/Filesystem.php b/tests/Feature/Helper/Filesystem.php index d867f5f..56e20ff 100644 --- a/tests/Feature/Helper/Filesystem.php +++ b/tests/Feature/Helper/Filesystem.php @@ -49,7 +49,7 @@ public static function createHelloWorldPHPFileWithSignalsHandling(): string pcntl_signal(SIGINT, "handler"); while (true) { - echo "Hello, world"; + echo "Hello, world\n"; sleep(1); } function handler($signal) { diff --git a/tests/Feature/SignalTest.php b/tests/Feature/SignalTest.php index 9c97482..fc942d4 100644 --- a/tests/Feature/SignalTest.php +++ b/tests/Feature/SignalTest.php @@ -18,13 +18,12 @@ public function it_sends_a_specified_signal_to_restart_the_app(): void } $scriptToRun = Filesystem::createHelloWorldPHPFileWithSignalsHandling(); - $this->watch($scriptToRun, ['--signal', 'SIGTERM', '--watch', Filesystem::fixturesDir()]); + $this->watch($scriptToRun, ['--watch', Filesystem::fixturesDir()]); $this->wait(); Filesystem::createHelloWorldPHPFile(); $this->wait(); - $this->wait(); - $this->assertOutputContains(SIGTERM.' signal was received'); + $this->assertOutputContains(SIGTERM.' restarting due to change'); } } From 77718237e6fc041c95a2e5ee9c8c2f31d0529e26 Mon Sep 17 00:00:00 2001 From: Sergey Zhuk Date: Fri, 24 Oct 2025 18:18:31 +0300 Subject: [PATCH 51/59] feat: use fswatch --- tests/Feature/SignalTest.php | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tests/Feature/SignalTest.php b/tests/Feature/SignalTest.php index fc942d4..6cc860d 100644 --- a/tests/Feature/SignalTest.php +++ b/tests/Feature/SignalTest.php @@ -13,17 +13,18 @@ final class SignalTest extends WatcherTestCase #[Test] public function it_sends_a_specified_signal_to_restart_the_app(): void { + $this->markTestSkipped('This test is not working on GitHub Actions'); if (! defined('SIGTERM') || ! extension_loaded('pcntl')) { $this->markTestSkipped('SIGTERM is not defined'); } $scriptToRun = Filesystem::createHelloWorldPHPFileWithSignalsHandling(); - $this->watch($scriptToRun, ['--watch', Filesystem::fixturesDir()]); + $this->watch($scriptToRun, ['--signal', 'SIGTERM', '--watch', Filesystem::fixturesDir()]); $this->wait(); Filesystem::createHelloWorldPHPFile(); $this->wait(); - $this->assertOutputContains(SIGTERM.' restarting due to change'); + $this->assertOutputContains(SIGTERM.' signal was received'); } } From bf12a01c0668cc97d74132d4307490d45b8dda54 Mon Sep 17 00:00:00 2001 From: Sergey Zhuk Date: Fri, 24 Oct 2025 18:19:41 +0300 Subject: [PATCH 52/59] feat: use fswatch --- phpunit.xml.dist | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 554bd7a..9b12865 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -6,7 +6,7 @@ convertNoticesToExceptions="true" convertWarningsToExceptions="true" processIsolation="false" - stopOnFailure="true" + stopOnFailure="false" bootstrap="vendor/autoload.php"> From 5c8305ba7bde867f5e4e92fabdb894322e484d7c Mon Sep 17 00:00:00 2001 From: Sergey Zhuk Date: Fri, 24 Oct 2025 18:20:57 +0300 Subject: [PATCH 53/59] feat: use fswatch --- tests/Feature/SignalTest.php | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/Feature/SignalTest.php b/tests/Feature/SignalTest.php index 6cc860d..a7aefec 100644 --- a/tests/Feature/SignalTest.php +++ b/tests/Feature/SignalTest.php @@ -13,7 +13,6 @@ final class SignalTest extends WatcherTestCase #[Test] public function it_sends_a_specified_signal_to_restart_the_app(): void { - $this->markTestSkipped('This test is not working on GitHub Actions'); if (! defined('SIGTERM') || ! extension_loaded('pcntl')) { $this->markTestSkipped('SIGTERM is not defined'); } From 8e9388aae6d1d43e42920b7b1aa73b8d0907b924 Mon Sep 17 00:00:00 2001 From: Sergey Zhuk Date: Fri, 24 Oct 2025 20:36:31 +0300 Subject: [PATCH 54/59] feat: use fswatch --- src/Filesystem/ChangesListener/FSWatchChangesListener.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Filesystem/ChangesListener/FSWatchChangesListener.php b/src/Filesystem/ChangesListener/FSWatchChangesListener.php index 24f2a62..449c603 100644 --- a/src/Filesystem/ChangesListener/FSWatchChangesListener.php +++ b/src/Filesystem/ChangesListener/FSWatchChangesListener.php @@ -37,6 +37,7 @@ public function start(WatchList $watchList): void $this->fsWatch = new FsWatch($this->makeOptions($watchList)); $this->fsWatch->run(); + $this->fsWatch->on('error', static fn ($error) => print_r($error)); $this->fsWatch->onChange(function (Change $fsWatchChange) use ($checkPathIsIgnored): void { $isIgnored = $checkPathIsIgnored($fsWatchChange->file()); if (! $isIgnored) { From 4a437b1470b6a2edaeaa083daa1ebb176af3ba02 Mon Sep 17 00:00:00 2001 From: Sergey Zhuk Date: Fri, 24 Oct 2025 22:18:28 +0300 Subject: [PATCH 55/59] feat: use fswatch --- .github/workflows/tests-fswatch.yml | 7 +- phpunit.xml.dist | 4 -- .../FSWatchChangesListener.php | 67 +++++++++++++------ .../FilesystemChangesListenerFactory.php | 2 +- tests/Feature/FsWatchChangesListenerTest.php | 2 +- tests/Feature/Helper/WatcherRunner.php | 2 +- 6 files changed, 56 insertions(+), 28 deletions(-) diff --git a/.github/workflows/tests-fswatch.yml b/.github/workflows/tests-fswatch.yml index b70ea6e..38e1dee 100644 --- a/.github/workflows/tests-fswatch.yml +++ b/.github/workflows/tests-fswatch.yml @@ -33,6 +33,11 @@ jobs: extensions: xdebug steps: + - name: Install Act dependencies + if: ${{ env.ACT }} + run: | + sudo apt-get update && apt-get install sudo -y + - name: Check Out Code uses: actions/checkout@v4 @@ -61,4 +66,4 @@ jobs: - name: Run tests with Phpunit run: | - XDEBUG_MODE=coverage php vendor/bin/phpunit --testsuite=ExceptFsWatchListener + php vendor/bin/phpunit diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 9b12865..36a7853 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -10,11 +10,7 @@ bootstrap="vendor/autoload.php"> - tests - - ./tests - ./tests/Feature/FsWatchChangesListenerTest.php diff --git a/src/Filesystem/ChangesListener/FSWatchChangesListener.php b/src/Filesystem/ChangesListener/FSWatchChangesListener.php index 449c603..08ea35f 100644 --- a/src/Filesystem/ChangesListener/FSWatchChangesListener.php +++ b/src/Filesystem/ChangesListener/FSWatchChangesListener.php @@ -5,12 +5,23 @@ namespace seregazhuk\PhpWatcher\Filesystem\ChangesListener; use Evenement\EventEmitter; +use React\EventLoop\LoopInterface; +use React\EventLoop\TimerInterface; use seregazhuk\PhpWatcher\Config\WatchList; -use Seregazhuk\ReactFsWatch\Change; use Seregazhuk\ReactFsWatch\FsWatch; +use Symfony\Component\Process\Process; final class FSWatchChangesListener extends EventEmitter implements ChangesListenerInterface { + private ?Process $process = null; + + private ?TimerInterface $timer = null; + + private const INTERVAL = 0.15; + + public function __construct(private readonly LoopInterface $loop) { + + } private ?FsWatch $fsWatch = null; public static function isAvailable(): bool @@ -35,15 +46,29 @@ public function start(WatchList $watchList): void return false; }; - $this->fsWatch = new FsWatch($this->makeOptions($watchList)); - $this->fsWatch->run(); - $this->fsWatch->on('error', static fn ($error) => print_r($error)); - $this->fsWatch->onChange(function (Change $fsWatchChange) use ($checkPathIsIgnored): void { - $isIgnored = $checkPathIsIgnored($fsWatchChange->file()); - if (! $isIgnored) { - $this->emit('change'); + $argsAndOptions = $this->makeOptions($watchList); + $this->process = new Process(command: ["fswatch", "-xrn", ...$argsAndOptions]); + $this->process->start(); + + $this->timer = $this->loop->addPeriodicTimer( + self::INTERVAL, + function () use ($checkPathIsIgnored): void { + $output = $this->process->getIncrementalOutput(); + if ($output === '') { + return; + } + $lines = explode("\n", $output); + foreach ($lines as $line) { + if ($line === '') { + continue; + } + [$path, ] = explode(' ', $line); + if (!$checkPathIsIgnored($path)) { + $this->emit('change'); + } + } } - }); + ); } public function onChange(callable $callback): void @@ -53,12 +78,16 @@ public function onChange(callable $callback): void public function stop(): void { - if ($this->fsWatch instanceof FsWatch) { - $this->fsWatch->stop(); + if ($this->process instanceof Process && $this->process->isRunning()) { + $this->process->stop(); + } + + if ($this->timer instanceof TimerInterface) { + $this->loop->cancelTimer($this->timer); } } - private function makeOptions(WatchList $watchList): string + private function makeOptions(WatchList $watchList): array { $options = []; @@ -72,9 +101,7 @@ private function makeOptions(WatchList $watchList): string $options = array_merge($options, $this->makeIncludeOptions($watchList)); } - $options[] = '-I'; // Case-insensitive - - return implode(' ', $options); + return $options; } /** @@ -83,15 +110,15 @@ private function makeOptions(WatchList $watchList): string private function makeIncludeOptions(WatchList $watchList): array { // Before including we need to ignore everything - $options[] = '-e ".*"'; + $options[] = '-e'; + $options[] = '.*'; + $options[] = '-i'; $regexpWithExtensions = array_map( - static fn ($extension): string => '"'.str_replace('*.', '.', $extension).'$"', + static fn ($extension): string => str_replace(['*.', '.'], '\\.', $extension).'$', $watchList->getFileExtensions() ); - $options[] = '-i '.implode(' ', $regexpWithExtensions); - - return $options; + return array_merge($options, $regexpWithExtensions); } public function getName(): string diff --git a/src/Filesystem/FilesystemChangesListenerFactory.php b/src/Filesystem/FilesystemChangesListenerFactory.php index def9f09..2df0a0d 100644 --- a/src/Filesystem/FilesystemChangesListenerFactory.php +++ b/src/Filesystem/FilesystemChangesListenerFactory.php @@ -17,7 +17,7 @@ final class FilesystemChangesListenerFactory public static function create(LoopInterface $loop, Screen $screen): ChangesListenerInterface { if (SystemRequirementsChecker::isFsWatchAvailable()) { - return new FsWatchChangesListener; + return new FsWatchChangesListener($loop); } if (SystemRequirementsChecker::isNodeJsInstalled()) { diff --git a/tests/Feature/FsWatchChangesListenerTest.php b/tests/Feature/FsWatchChangesListenerTest.php index 0b7a60e..00f7247 100644 --- a/tests/Feature/FsWatchChangesListenerTest.php +++ b/tests/Feature/FsWatchChangesListenerTest.php @@ -26,7 +26,7 @@ public function it_emits_change_event_on_changes(): void } $loop = Loop::get(); - $listener = new FSWatchChangesListener; + $listener = new FSWatchChangesListener($loop); $eventWasEmitted = false; $listener->onChange(function () use (&$eventWasEmitted): void { diff --git a/tests/Feature/Helper/WatcherRunner.php b/tests/Feature/Helper/WatcherRunner.php index 1f936a5..960993e 100644 --- a/tests/Feature/Helper/WatcherRunner.php +++ b/tests/Feature/Helper/WatcherRunner.php @@ -13,7 +13,7 @@ final class WatcherRunner */ public function run(string $scriptToRun, array $arguments = []): Process { - $arguments = array_merge($arguments, ['--delay', 0.25]); + $arguments = array_merge($arguments, ['--delay', 1]); $process = new Process(array_merge(['./php-watcher', $scriptToRun], $arguments)); $process->start(); From 90ac5dae65ede5aaa54370dbb68e7ae45e5e0889 Mon Sep 17 00:00:00 2001 From: Sergey Zhuk Date: Sat, 25 Oct 2025 10:14:41 +0300 Subject: [PATCH 56/59] feat: use fswatch --- .github/workflows/tests-fswatch.yml | 7 +++---- .../FilesystemChangesListenerFactory.php | 2 +- .../SystemRequirementsChecker.php | 15 ++++++++++----- 3 files changed, 14 insertions(+), 10 deletions(-) diff --git a/.github/workflows/tests-fswatch.yml b/.github/workflows/tests-fswatch.yml index 38e1dee..d85dd6f 100644 --- a/.github/workflows/tests-fswatch.yml +++ b/.github/workflows/tests-fswatch.yml @@ -18,7 +18,7 @@ on: - 'README.md' jobs: - tests: + tests-fswatch: timeout-minutes: 5 name: Tests with FsWatch (PHP ${{ matrix.php }}, OS ${{ matrix.os }}) @@ -26,9 +26,8 @@ jobs: strategy: matrix: - os: [ubuntu-latest] + os: [macos-latest ] php: [8.1, 8.2, 8.3, 8.4] - env: extensions: xdebug @@ -66,4 +65,4 @@ jobs: - name: Run tests with Phpunit run: | - php vendor/bin/phpunit + php vendor/bin/phpunit diff --git a/src/Filesystem/FilesystemChangesListenerFactory.php b/src/Filesystem/FilesystemChangesListenerFactory.php index 2df0a0d..9185f5f 100644 --- a/src/Filesystem/FilesystemChangesListenerFactory.php +++ b/src/Filesystem/FilesystemChangesListenerFactory.php @@ -16,7 +16,7 @@ final class FilesystemChangesListenerFactory { public static function create(LoopInterface $loop, Screen $screen): ChangesListenerInterface { - if (SystemRequirementsChecker::isFsWatchAvailable()) { + if (SystemRequirementsChecker::isFSWatchAvailable()) { return new FsWatchChangesListener($loop); } diff --git a/src/SystemRequirements/SystemRequirementsChecker.php b/src/SystemRequirements/SystemRequirementsChecker.php index d68a889..999c3dd 100644 --- a/src/SystemRequirements/SystemRequirementsChecker.php +++ b/src/SystemRequirements/SystemRequirementsChecker.php @@ -4,14 +4,19 @@ namespace seregazhuk\PhpWatcher\SystemRequirements; -use Seregazhuk\ReactFsWatch\FsWatch; use Symfony\Component\Process\Process; final class SystemRequirementsChecker { - public static function isFsWatchAvailable(): bool + /** + * Works correctly only on OSX. + */ + public static function isFSWatchAvailable(): bool { - return FsWatch::isAvailable(); + $process = new Process(command: ['fswatch', '--version']); + $process->start(); + $process->wait(); + return $process->isSuccessful() && PHP_OS_FAMILY === 'Darwin'; } public static function isNodeJsInstalled(): bool @@ -20,7 +25,7 @@ public static function isNodeJsInstalled(): bool $process->start(); $process->wait(); - return $process->getExitCode() === 0; + return $process->isSuccessful(); } public static function isChokidarInstalled(): bool @@ -29,7 +34,7 @@ public static function isChokidarInstalled(): bool $process->start(); $process->wait(); - return $process->getExitCode() === 0 && str_contains($process->getOutput(), 'chokidar'); + return $process->isSuccessful() && str_contains($process->getOutput(), 'chokidar'); } public static function installChokidar(): void From cd16c475f6a856fb25a4d4ef194d45ce0d173bef Mon Sep 17 00:00:00 2001 From: Sergey Zhuk Date: Sat, 25 Oct 2025 10:15:59 +0300 Subject: [PATCH 57/59] feat: use fswatch --- .github/workflows/tests-fswatch.yml | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/.github/workflows/tests-fswatch.yml b/.github/workflows/tests-fswatch.yml index d85dd6f..7081911 100644 --- a/.github/workflows/tests-fswatch.yml +++ b/.github/workflows/tests-fswatch.yml @@ -32,16 +32,11 @@ jobs: extensions: xdebug steps: - - name: Install Act dependencies - if: ${{ env.ACT }} - run: | - sudo apt-get update && apt-get install sudo -y - - name: Check Out Code uses: actions/checkout@v4 - name: Install fswatch - run: sudo apt-get install fswatch + run: brew install fswatch - name: Setup PHP ${{ matrix.php }} uses: shivammathur/setup-php@v2 From e234a5c26478b3cc61a8f9e9d99a68bdd783973e Mon Sep 17 00:00:00 2001 From: Sergey Zhuk Date: Sat, 25 Oct 2025 10:20:45 +0300 Subject: [PATCH 58/59] feat: use fswatch --- .github/workflows/security.yml | 2 +- .github/workflows/tests-chokidar.yml | 2 +- .github/workflows/tests-fswatch.yml | 2 +- README.md | 5 +++-- .../ChangesListener/FSWatchChangesListener.php | 12 +++++------- src/SystemRequirements/SystemRequirementsChecker.php | 1 + 6 files changed, 12 insertions(+), 12 deletions(-) diff --git a/.github/workflows/security.yml b/.github/workflows/security.yml index e68c452..6b2e33d 100644 --- a/.github/workflows/security.yml +++ b/.github/workflows/security.yml @@ -36,7 +36,7 @@ jobs: uses: shivammathur/setup-php@v2 with: php-version: ${{ matrix.php }} - extensions: dom, sockets, grpc, curl + extensions: pcntl - name: Check Out Code uses: actions/checkout@v4 diff --git a/.github/workflows/tests-chokidar.yml b/.github/workflows/tests-chokidar.yml index 2e9d84e..5a4fcac 100644 --- a/.github/workflows/tests-chokidar.yml +++ b/.github/workflows/tests-chokidar.yml @@ -49,7 +49,7 @@ jobs: uses: shivammathur/setup-php@v2 with: php-version: ${{ matrix.php }} - extensions: dom, sockets, pcntl + extensions: pcntl - name: Validate composer.json and composer.lock run: composer validate --strict diff --git a/.github/workflows/tests-fswatch.yml b/.github/workflows/tests-fswatch.yml index 7081911..609fe38 100644 --- a/.github/workflows/tests-fswatch.yml +++ b/.github/workflows/tests-fswatch.yml @@ -42,7 +42,7 @@ jobs: uses: shivammathur/setup-php@v2 with: php-version: ${{ matrix.php }} - extensions: dom, sockets, pcntl + extensions: pcntl - name: Validate composer.json and composer.lock run: composer validate --strict diff --git a/README.md b/README.md index 9db9191..864bcf8 100644 --- a/README.md +++ b/README.md @@ -239,11 +239,12 @@ script crashes PHP-watcher will notify you about that. The watcher can use different strategies to monitor your file system changes. Under the hood it detects the environment and chooses the best suitable strategy. -### Fswatch +### Fswatch (OSX only) [FsWatch](https://github.com/emcrisostomo/fswatch) is a cross-platform (Linux,Mac,Windows) file change monitor that will automatically use the platforms native functionality when possible. Under the hood the filesystem notifies us -when any changes occur. If your system has fswatch installed, this strategy will be used. +when any changes occur. Currently, it [doesn't work correctly on Linux](https://github.com/emcrisostomo/fswatch/issues/247). +If your system is OSx and has fswatch installed, this strategy will be used. **Has not been extensively tested.** diff --git a/src/Filesystem/ChangesListener/FSWatchChangesListener.php b/src/Filesystem/ChangesListener/FSWatchChangesListener.php index 08ea35f..6235778 100644 --- a/src/Filesystem/ChangesListener/FSWatchChangesListener.php +++ b/src/Filesystem/ChangesListener/FSWatchChangesListener.php @@ -19,10 +19,7 @@ final class FSWatchChangesListener extends EventEmitter implements ChangesListen private const INTERVAL = 0.15; - public function __construct(private readonly LoopInterface $loop) { - - } - private ?FsWatch $fsWatch = null; + public function __construct(private readonly LoopInterface $loop) {} public static function isAvailable(): bool { @@ -47,7 +44,7 @@ public function start(WatchList $watchList): void }; $argsAndOptions = $this->makeOptions($watchList); - $this->process = new Process(command: ["fswatch", "-xrn", ...$argsAndOptions]); + $this->process = new Process(command: ['fswatch', '-xrn', ...$argsAndOptions]); $this->process->start(); $this->timer = $this->loop->addPeriodicTimer( @@ -62,8 +59,8 @@ function () use ($checkPathIsIgnored): void { if ($line === '') { continue; } - [$path, ] = explode(' ', $line); - if (!$checkPathIsIgnored($path)) { + [$path] = explode(' ', $line); + if (! $checkPathIsIgnored($path)) { $this->emit('change'); } } @@ -118,6 +115,7 @@ private function makeIncludeOptions(WatchList $watchList): array static fn ($extension): string => str_replace(['*.', '.'], '\\.', $extension).'$', $watchList->getFileExtensions() ); + return array_merge($options, $regexpWithExtensions); } diff --git a/src/SystemRequirements/SystemRequirementsChecker.php b/src/SystemRequirements/SystemRequirementsChecker.php index 999c3dd..c219b49 100644 --- a/src/SystemRequirements/SystemRequirementsChecker.php +++ b/src/SystemRequirements/SystemRequirementsChecker.php @@ -16,6 +16,7 @@ public static function isFSWatchAvailable(): bool $process = new Process(command: ['fswatch', '--version']); $process->start(); $process->wait(); + return $process->isSuccessful() && PHP_OS_FAMILY === 'Darwin'; } From 36c4ef988312903f1750ef94e035059a45b3b10a Mon Sep 17 00:00:00 2001 From: Sergey Zhuk Date: Sat, 25 Oct 2025 10:28:26 +0300 Subject: [PATCH 59/59] feat: use fswatch --- src/Filesystem/ChangesListener/FSWatchChangesListener.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/Filesystem/ChangesListener/FSWatchChangesListener.php b/src/Filesystem/ChangesListener/FSWatchChangesListener.php index 6235778..2435b00 100644 --- a/src/Filesystem/ChangesListener/FSWatchChangesListener.php +++ b/src/Filesystem/ChangesListener/FSWatchChangesListener.php @@ -84,6 +84,9 @@ public function stop(): void } } + /** + * @return string[] + */ private function makeOptions(WatchList $watchList): array { $options = [];