Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Watch Files Laravel Package

crodas/watch-files

A lightweight PHP file watcher that monitors files and directories for changes, firing callbacks when they are created, modified, or deleted. Useful for auto-rebuilding assets, clearing caches, or rerunning tests during development without relying on external tools.

View on GitHub
Deep Wiki
Context7

Getting Started

crodas/watch-files is a small, stateless watcher: you point it at files/directories, it invokes a callback when something changes. There's no daemon, no persistent state file, and no event history — each poll/check is independent, which is exactly what makes it easy to drop into long-running Artisan commands.

Install and locate the API first

composer require crodas/watch-files --dev

Open vendor/crodas/watch-files/README.md and the src/ directory before writing any code — with only ~9 stars and no dependents, the public API is minimal and lightly documented, so read the actual class signatures rather than assuming a Laravel-style fluent interface.

First use case: an Artisan watcher command

The most common day-to-day pattern is a php artisan watch style command that reacts to file changes:

namespace App\Console\Commands;

use Illuminate\Console\Command;
use WatchFiles\WatchFiles;

class Watch extends Command
{
    protected $signature = 'watch {path=app}';
    protected $description = 'Watch application files for changes';

    public function handle()
    {
        $path = base_path($this->argument('path'));

        $this->info("Watching {$path}...");

        (new WatchFiles())->watch($path, function ($file) {
            $this->line("[" . now()->format('H:i:s') . "] changed: {$file}");
            // trigger rebuild, rerun tests, clear cache, etc.
        })->run();
    }
}

Verify the exact entry point against the README — depending on the version, the watcher may be constructed differently or run via an iterator rather than ->run(). Confirm with:

php -r 'require "vendor/autoload.php"; print_r(get_class_methods(WatchFiles\WatchFiles::class));'

Start with a single directory (app/), a single change type, and a callback that only logs. Add the "do something" step once you trust the trigger.

Implementation Patterns

1. Artisan command + Symfony Process for the action

Keep the callback thin; shell out for the actual work so failures are isolated:

->watch(base_path('app'), function ($file) {
    $this->getOutput()->writeln("[watch] {$file}");

    // Debounce a burst of writes from an editor save
    usleep(150_000);

    $process = new \Symfony\Component\Process\Process(
        ['php', 'artisan', 'test', '--filter=Unit']
    );
    $process->setTimeout(120);
    $process->run();
    $this->getOutput()->write($process->getOutput());
});

Laravel's own Symfony\Component\Process (via Illuminate\Support\Facades\Process in newer versions) is the cleanest integration — it gives you timeout, working directory, and exit code handling without reimplementing it.

2. Compose with Laravel's cache/filesystem for state

Because the package is stateless, you own any debounce or dedupe logic. A useful pattern is to guard on an mtime/md5 stored in the cache:

->watch(storage_path('logs'), function ($file) use ($cache) {
    $key = 'watch:' . md5($file);
    $hash = md5_file($file);

    if ($cache->get($key) === $hash) {
        return; // no real change, just a touched file
    }
    $cache->put($key, $hash, 300);

    // ... actual reaction
});

This is the single most important pattern with a stateless watcher — otherwise you'll fire your action multiple times per save.

3. Triggering hot-reload signals in local dev

Pair the watcher with a lightweight signal file your front-end reads, or call artisan view:clear, artisan cache:clear, or vite via process:

->watch([base_path('resources/views'), base_path('routes')], function () {
    Process::run('php artisan view:clear')->throw();
    Process::run('php artisan route:clear')->throw();
});

4. Watching for background job / queue side effects

Useful for tail-style workflows: watch storage/logs/laravel.log and surface new lines, or watch database/migrations and auto-apply. For the log case, track your offset in the cache so you only print new bytes.

5. Integrating with queue workers in local dev

When you're not running php artisan queue:work, you can use the watcher to nudge a queue by writing to a signal file the worker checks — cleaner than polling the DB.

6. Batch multiple directories

Pass an array of paths rather than nesting watchers. Nested watchers multiply your polling cost and make callback ordering hard to reason about.

Integration tips

  • Register the command as hidden or gate it behind App::environment('local') in app/Console/Kernel.php — a production-accidental php artisan watch is painful.
  • Log to a dedicated channel (Log::channel('watch')) so a runaway callback doesn't flood the default log.
  • Use $this->getOutput() rather than echo so --quiet and -v behave correctly.
  • If you already use Supervisor/inotifywait, keep the watcher for portable dev (macOS/Windows) and let production rely on the OS tooling.

Gotchas and Tips

Polling cost is real. With no daemon, "stateless" usually means each check stats a set of files. Pointing it at vendor/ or node_modules/ will pin a core. Always exclude those — pass a narrow list of directories or filter in the callback.

No built-in debounce. A single editor save can produce several events (temp file, rename, write). Guard with mtime hashing (see above) or a short usleep. Without this, your watcher will launch tests three times per keystroke-save.

Callback exceptions kill the loop. Wrap the callback body in try/catch and log; an uncaught exception in a test runner invocation will terminate the command and you'll silently lose the watcher. Example:

try {
    $process->run();
    if ($process->getExitCode() > 0) {
        $this->error($process->getErrorOutput());
    }
} catch (\Throwable $e) {
    Log::channel('watch')->error($e);
}

Long-running process memory. Callbacks that collect file contents, log lines, or process output will accumulate. Clear per-iteration variables, cap any arrays, and consider gc_collect_cycles() every N iterations if you see growth.

Editor-agnostic change detection. Some editors write via rename rather than in-place modification; depending on the underlying mechanism, this can look like two changes or a delete+create. Test with your actual editor (vim, VS Code, and PHPStorm all differ).

Symlinks. If your project symlinks storage/ (Laravel Forge/Envoyer pattern), verify the watcher follows them. In polling mode it typically will; with inotify-backed paths it often won't. Watch the resolved path when in doubt.

inotify watch limits. If the package uses inotify under the hood and you exceed fs.inotify.max_user_watches, the watcher can fail silently or drop directories. Check cat /proc/sys/fs/inotify/max_user_watches on Linux.

Extension point: the callback receives the changed path — that's your hook. Route by extension (*.php → tests, *.blade.php → view:clear, *.env → config:clear) in a single dispatcher rather than constructing multiple watchers.

Windows. If you're on a Laravel dev team mixing OSes, test on Windows early — file notification semantics (and the availability of inotify) differ enough that the same command may behave differently across the team.

Maturity hedge. Nine stars and zero dependents means you're on your own for edge cases. Pin the version in composer.json, keep the usage behind a single Watch command class, and don't rely on it anywhere on the production request path — treat it as developer tooling, not infrastructure.

Debugging the watcher itself: run with -vvv, add a file_put_contents('/tmp/watch.log', ...) inside the callback, and confirm with strace -f -e trace=stat or inotifywait that events are actually arriving — if the package never fires, the issue is usually the path form (relative vs. absolute) you handed it.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
coleus/frame
thtg88/journalism
filagora/filagora-ai-forums
rlacerda83/query-parser
socialdept/atp-orm
cboxdk/laravel-assistant-client
socialdept/atp-resolver
socialdept/tenant-domains
tautvydasr/blade-class-names
socialdept/color-scales
maherelgamil/controller
coleus/widgets
coleus/table
coleus/users
coleus/music
coleus/health
coleus/calendar
coleus/notes
coleus/support
coleus/settings