├── package.json ├── src ├── Exceptions │ └── CouldNotStartWatcher.php └── Watch.php ├── bin └── file-watcher.js ├── LICENSE.md ├── composer.json └── README.md /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "devDependencies": {}, 3 | "dependencies": { 4 | "chokidar": "4" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /src/Exceptions/CouldNotStartWatcher.php: -------------------------------------------------------------------------------- 1 | getErrorOutput()); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /bin/file-watcher.js: -------------------------------------------------------------------------------- 1 | const chokidar = require('chokidar'); 2 | 3 | const paths = JSON.parse(process.argv[2]); 4 | 5 | const watcher = chokidar.watch(paths, { 6 | ignoreInitial: true, 7 | awaitWriteFinish: { 8 | stabilityThreshold: 2000, 9 | pollInterval: 100 10 | } 11 | }); 12 | 13 | watcher 14 | .on('add', path => console.log(`fileCreated - ${path}`)) 15 | .on('change', path => console.log(`fileUpdated - ${path}`)) 16 | .on('unlink', path => console.log(`fileDeleted - ${path}`)) 17 | .on('addDir', path => console.log(`directoryCreated - ${path}`)) 18 | .on('unlinkDir', path => console.log(`directoryDeleted - ${path}`)) 19 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) spatie 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "spatie/file-system-watcher", 3 | "description": "Watch changes in the file system using PHP", 4 | "keywords": [ 5 | "spatie", 6 | "file-system-watcher" 7 | ], 8 | "homepage": "https://github.com/spatie/file-system-watcher", 9 | "license": "MIT", 10 | "authors": [ 11 | { 12 | "name": "Freek Van der Herten", 13 | "email": "freek@spatie.be", 14 | "role": "Developer" 15 | } 16 | ], 17 | "require": { 18 | "php": "^8.3", 19 | "symfony/process": "^7.0|^8.0" 20 | }, 21 | "require-dev": { 22 | "pestphp/pest": "^4.0", 23 | "spatie/ray": "^1.22", 24 | "spatie/temporary-directory": "^2.0" 25 | }, 26 | "autoload": { 27 | "psr-4": { 28 | "Spatie\\Watcher\\": "src" 29 | } 30 | }, 31 | "autoload-dev": { 32 | "psr-4": { 33 | "Spatie\\Watcher\\Tests\\": "tests" 34 | } 35 | }, 36 | "scripts": { 37 | "test": "vendor/bin/pest", 38 | "test-coverage": "vendor/bin/pest --coverage-html coverage", 39 | "format": "vendor/bin/php-cs-fixer fix --allow-risky=yes" 40 | }, 41 | "config": { 42 | "sort-packages": true, 43 | "allow-plugins": { 44 | "pestphp/pest-plugin": true 45 | } 46 | }, 47 | "minimum-stability": "dev", 48 | "prefer-stable": true 49 | } 50 | -------------------------------------------------------------------------------- /src/Watch.php: -------------------------------------------------------------------------------- 1 | setPaths($path); 44 | } 45 | 46 | public static function paths(...$paths): self 47 | { 48 | return (new self())->setPaths($paths); 49 | } 50 | 51 | public function __construct() 52 | { 53 | $this->shouldContinue = fn () => true; 54 | } 55 | 56 | public function setPaths(string | array $paths): self 57 | { 58 | if (is_string($paths)) { 59 | $paths = func_get_args(); 60 | } 61 | 62 | $this->paths = $paths; 63 | 64 | return $this; 65 | } 66 | 67 | public function onFileCreated(callable $onFileCreated): self 68 | { 69 | $this->onFileCreated[] = $onFileCreated; 70 | 71 | return $this; 72 | } 73 | 74 | public function onFileUpdated(callable $onFileUpdated): self 75 | { 76 | $this->onFileUpdated[] = $onFileUpdated; 77 | 78 | return $this; 79 | } 80 | 81 | public function onFileDeleted(callable $onFileDeleted): self 82 | { 83 | $this->onFileDeleted[] = $onFileDeleted; 84 | 85 | return $this; 86 | } 87 | 88 | public function onDirectoryCreated(callable $onDirectoryCreated): self 89 | { 90 | $this->onDirectoryCreated[] = $onDirectoryCreated; 91 | 92 | return $this; 93 | } 94 | 95 | public function onDirectoryDeleted(callable $onDirectoryDeleted): self 96 | { 97 | $this->onDirectoryDeleted[] = $onDirectoryDeleted; 98 | 99 | return $this; 100 | } 101 | 102 | public function onAnyChange(callable $callable): self 103 | { 104 | $this->onAny[] = $callable; 105 | 106 | return $this; 107 | } 108 | 109 | public function setIntervalTime(int $interval): self 110 | { 111 | $this->interval = $interval; 112 | 113 | return $this; 114 | } 115 | 116 | public function shouldContinue(Closure $shouldContinue): self 117 | { 118 | $this->shouldContinue = $shouldContinue; 119 | 120 | return $this; 121 | } 122 | 123 | public function start(): void 124 | { 125 | $watcher = $this->getWatchProcess(); 126 | 127 | while (true) { 128 | if (! $watcher->isRunning()) { 129 | throw CouldNotStartWatcher::make($watcher); 130 | } 131 | 132 | if ($output = $watcher->getIncrementalOutput()) { 133 | $this->actOnOutput($output); 134 | } 135 | 136 | if (! ($this->shouldContinue)()) { 137 | break; 138 | } 139 | 140 | usleep($this->interval); 141 | } 142 | } 143 | 144 | protected function getWatchProcess(): Process 145 | { 146 | $command = [ 147 | (new ExecutableFinder)->find('node'), 148 | realpath(__DIR__ . '/../bin/file-watcher.js'), 149 | json_encode($this->paths), 150 | ]; 151 | 152 | $process = new Process( 153 | command: $command, 154 | timeout: null, 155 | ); 156 | 157 | $process->start(); 158 | 159 | return $process; 160 | } 161 | 162 | protected function actOnOutput(string $output): void 163 | { 164 | $lines = explode(PHP_EOL, $output); 165 | 166 | $lines = array_filter($lines); 167 | 168 | foreach ($lines as $line) { 169 | [$type, $path] = explode(' - ', $line, 2); 170 | 171 | $path = trim($path); 172 | 173 | match ($type) { 174 | static::EVENT_TYPE_FILE_CREATED => $this->callAll($this->onFileCreated, $path), 175 | static::EVENT_TYPE_FILE_UPDATED => $this->callAll($this->onFileUpdated, $path), 176 | static::EVENT_TYPE_FILE_DELETED => $this->callAll($this->onFileDeleted, $path), 177 | static::EVENT_TYPE_DIRECTORY_CREATED => $this->callAll($this->onDirectoryCreated, $path), 178 | static::EVENT_TYPE_DIRECTORY_DELETED => $this->callAll($this->onDirectoryDeleted, $path), 179 | }; 180 | 181 | foreach ($this->onAny as $onAnyCallable) { 182 | $onAnyCallable($type, $path); 183 | } 184 | } 185 | } 186 | 187 | protected function callAll(array $callables, string $path): void 188 | { 189 | foreach ($callables as $callable) { 190 | $callable($path); 191 | } 192 | } 193 | } 194 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Watch changes in the file system using PHP 2 | 3 | [![Latest Version on Packagist](https://img.shields.io/packagist/v/spatie/file-system-watcher.svg?style=flat-square)](https://packagist.org/packages/spatie/file-system-watcher) 4 | [![Tests](https://github.com/spatie/file-system-watcher/actions/workflows/run-tests.yml/badge.svg)](https://github.com/spatie/file-system-watcher/actions/workflows/run-tests.yml) 5 | [![Total Downloads](https://img.shields.io/packagist/dt/spatie/file-system-watcher.svg?style=flat-square)](https://packagist.org/packages/spatie/file-system-watcher) 6 | 7 | This package allows you to react to all kinds of changes in the file system. 8 | 9 | Here's how you can run code when a new file gets added. 10 | 11 | ```php 12 | use Spatie\Watcher\Watch; 13 | 14 | Watch::path($directory) 15 | ->onFileCreated(function (string $newFilePath) { 16 | // do something... 17 | }) 18 | ->start(); 19 | ``` 20 | 21 | ## Support us 22 | 23 | [](https://spatie.be/github-ad-click/file-system-watcher) 24 | 25 | We invest a lot of resources into creating [best in class open source packages](https://spatie.be/open-source). You can support us by [buying one of our paid products](https://spatie.be/open-source/support-us). 26 | 27 | We highly appreciate you sending us a postcard from your hometown, mentioning which of our package(s) you are using. You'll find our address on [our contact page](https://spatie.be/about-us). We publish all received postcards on [our virtual postcard wall](https://spatie.be/open-source/postcards). 28 | 29 | ## Installation 30 | 31 | You can install the package via composer: 32 | 33 | ```bash 34 | composer require spatie/file-system-watcher 35 | ``` 36 | 37 | In your project, you should have the JavaScript package [`chokidar`](https://github.com/paulmillr/chokidar) installed. You can install it via npm 38 | 39 | ```bash 40 | npm install chokidar 41 | ``` 42 | 43 | or Yarn 44 | 45 | ```bash 46 | yarn add chokidar 47 | ``` 48 | 49 | ## Usage 50 | 51 | Here's how you can start watching a directory and get notified of any changes. 52 | 53 | ```php 54 | use Spatie\Watcher\Watch; 55 | 56 | Watch::path($directory) 57 | ->onAnyChange(function (string $type, string $path) { 58 | if ($type === Watch::EVENT_TYPE_FILE_CREATED) { 59 | echo "file {$path} was created"; 60 | } 61 | }) 62 | ->start(); 63 | ``` 64 | 65 | You can pass as many directories as you like to `path`. 66 | 67 | To start watching, call the `start` method. Note that the `start` method will never end. Any code after that will not be executed. 68 | 69 | To make sure that the watcher keeps watching in production, monitor the script or command that starts it with something like [Supervisord](http://supervisord.org). See [Supervisord example configuration](#supervisord-example-configuration) below. 70 | 71 | ### Detected the type of change 72 | 73 | The `$type` parameter of the closure you pass to `onAnyChange` can contain one of these values: 74 | 75 | - `Watcher::EVENT_TYPE_FILE_CREATED`: a file was created 76 | - `Watcher::EVENT_TYPE_FILE_UPDATED`: a file was updated 77 | - `Watcher::EVENT_TYPE_FILE_DELETED`: a file was deleted 78 | - `Watcher::EVENT_TYPE_DIRECTORY_CREATED`: a directory was created 79 | - `Watcher::EVENT_TYPE_DIRECTORY_DELETED`: a directory was deleted 80 | 81 | ### Listening for specific events 82 | 83 | To handle file systems events of a certain type, you can make use of dedicated functions. Here's how you would listen for file creations only. 84 | 85 | ```php 86 | use Spatie\Watcher\Watch; 87 | 88 | Watch::path($directory) 89 | ->onFileCreated(function (string $newFilePath) { 90 | // do something... 91 | }); 92 | ``` 93 | 94 | These are the related available methods: 95 | 96 | - `onFileCreated()`: accepts a closure that will get passed the new file path 97 | - `onFileUpdated()`: accepts a closure that will get passed the updated file path 98 | - `onFileDeleted()`: accepts a closure that will get passed the deleted file path 99 | - `onDirectoryCreated()`: accepts a closure that will get passed the created directory path 100 | - `onDirectoryDeleted()`: accepts a closure that will get passed the deleted directory path 101 | 102 | ### Watching multiple paths 103 | 104 | You can pass multiple paths to the `paths` method. 105 | 106 | ```php 107 | use Spatie\Watcher\Watch; 108 | 109 | Watch::paths($directory, $anotherDirectory); 110 | ``` 111 | 112 | ### Performing multiple tasks 113 | 114 | You can call `onAnyChange`, 'onFileCreated', ... multiple times. All given closures will be performed 115 | 116 | ```php 117 | use Spatie\Watcher\Watch; 118 | 119 | Watch::path($directory) 120 | ->onFileCreated(function (string $newFilePath) { 121 | // do something on file creation... 122 | }) 123 | ->onFileCreated(function (string $newFilePath) { 124 | // do something else on file creation... 125 | }) 126 | ->onAnyChange(function (string $type, string $path) { 127 | // do something... 128 | }) 129 | ->onAnyChange(function (string $type, string $path) { 130 | // do something else... 131 | }) 132 | // ... 133 | ``` 134 | 135 | ### Stopping the watcher gracefully 136 | 137 | By default, the watcher will continue indefinitely when started. To gracefully stop the watcher, you can call `shouldContinue` and pass it a closure. If the closure returns a falsy value, the watcher will stop. The given closure will be executed every 0.5 second. 138 | 139 | ```php 140 | use Spatie\Watcher\Watch; 141 | 142 | Watch::path($directory) 143 | ->shouldContinue(function () { 144 | // return true or false 145 | }) 146 | // ... 147 | ``` 148 | 149 | ### Change the speed of watcher 150 | 151 | By default, the changes are tracked every 0.5 seconds, however you could change that. 152 | 153 | ```php 154 | use Spatie\Watcher\Watch; 155 | 156 | Watch::path($directory) 157 | ->setIntervalTime(1000000) //unit is microsecond therefore -> 0.1s 158 | // ...rest of your methods 159 | ``` 160 | 161 | Notice : there is no file watching based on polling going on. 162 | 163 | ## Testing 164 | 165 | ```bash 166 | composer test 167 | ``` 168 | 169 | ## Supervisord example configuration 170 | 171 | Create a new Supervisord configuration to monitor a Laravel artisan command which calls the watcher. While using Supervisord, you must specicfy your Node.js and PHP executables in your command paramater: `env PATH="/usr/local/bin"` for Node.js, the absolute path to PHP and your project's path. 172 | 173 | 174 | ``` 175 | [program:watch] 176 | process_name=%(program_name)s 177 | directory=/your/project 178 | command=env PATH="/usr/local/bin" /absolute/path/to/php /your/project/artisan watch-for-files 179 | autostart=true 180 | autorestart=false 181 | user=username 182 | redirect_stderr=true 183 | stdout_logfile=/your/project/storage/logs/watch.log 184 | stopwaitsecs=3600 185 | ``` 186 | 187 | ## Changelog 188 | 189 | Please see [CHANGELOG](CHANGELOG.md) for more information on what has changed recently. 190 | 191 | ## Contributing 192 | 193 | Please see [CONTRIBUTING](https://github.com/spatie/.github/blob/main/CONTRIBUTING.md) for details. 194 | 195 | ## Security Vulnerabilities 196 | 197 | Please review [our security policy](../../security/policy) on how to report security vulnerabilities. 198 | 199 | ## Credits 200 | 201 | - [Freek Van der Herten](https://github.com/freekmurze) 202 | - [All Contributors](../../contributors) 203 | 204 | Parts of this package are inspired by [Laravel Octane](https://github.com/laravel/octane) 205 | 206 | ## License 207 | 208 | The MIT License (MIT). Please see [License File](LICENSE.md) for more information. 209 | --------------------------------------------------------------------------------