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

Resource Watcher Laravel Package

cartalyst/resource-watcher

Resource Watcher lets Laravel apps monitor files and other resources for changes, dispatching events when modifications are detected. It offers a simple watcher API with periodic checks, handy for cache busting, asset tracking, or reacting to config and template updates.

View on GitHub
Deep Wiki
Context7

Getting Started

Check the runtime before writing any code. Almost every "it doesn't fire" issue with this package is environmental, not a bug in your integration:

composer require cartalyst/resource-watcher
php -m | grep inotify        # must print "inotify"

If it prints nothing, install the extension (pecl install inotify, then extension=inotify.so in your CLI php.ini). On macOS and Windows inotify does not exist — stop here and either use a polling watcher (yosymfony/resource-watcher) or, for frontend assets, just let Vite/Node do the watching. Do not spend an afternoon debugging code that can't work on your OS.

Read src/ first. The README is thin, the package has 7 stars and zero dependents, so there are no write-ups or Stack Overflow answers to lean on. The public surface is small — a watcher object, event constants for created/modified/deleted, and a blocking start() loop — but the exact class and constant names matter. Confirm them in source rather than trusting any snippet, including these.

First use case: one directory, one throwaway script. Do not begin with Laravel service-provider wiring. Prove inotify works end to end:

require __DIR__.'/vendor/autoload.php';

$watcher = new ResourceWatcher;
$watcher->watch(__DIR__.'/resources/views');

$watcher->on('resource.modified', function ($event) {
    echo "changed: ".$event->getResource().PHP_EOL;
});

$watcher->start(); // blocks forever

Run it, touch a file in the watched directory, and confirm output. Only after that works should you wrap it in an Artisan command.

Then, and only then, Laravel. Register the service provider if the package ships one (verify in src/, the "Laravel package" framing may be aspirational), publish any config, and move the loop into a dedicated php artisan watch:* command. Keep it in the console layer — no domain code should ever import the package namespace.

Implementation Patterns

Adapter-first wrapping. Define your own small contract and keep the package behind it. This is the single most valuable pattern with a low-signal dependency:

interface ResourceWatcherContract
{
    public function watch(string|array $paths): self;
    public function on(string $event, callable $handler): self;
    public function start(): void;
}

Your Artisan command, your jobs, and your tests talk to this interface. Swapping to spatie/file-system-watcher or ripping it out entirely becomes a one-file change, and you can write a FakeResourceWatcher for unit tests without touching a real inotify loop.

Artisan command as the entry point. Long-running watchers belong in a console command, not a service provider boot method or a queued job:

class WatchViews extends Command
{
    protected $signature = 'watch:views';

    public function handle(ResourceWatcherContract $watcher): int
    {
        $watcher->watch(base_path('resources/views'));
        $watcher->on('resource.modified', fn ($e) => $this->line('changed: '.$e->getResource()));
        $watcher->start();

        return self::SUCCESS;
    }
}

Bind the contract to a package-backed implementation in a service provider, and to a fake in tests. The command never knows which watcher it has.

Debounce and coalesce at the boundary, not in the handler. Editors do atomic saves (write temp file, rename over the original), which surface as delete+create rather than modify — a single Cmd+S can fire your handler three to eight times. Compilers touch whole trees. Put the throttle in one place:

$seen = [];

$watcher->on('resource.modified', function ($event) use (&$seen) {
    $path = $event->getResource();
    $now  = microtime(true);

    if (isset($seen[$path]) && ($now - $seen[$path]) < 0.3) {
        return; // coalesce bursts
    }

    $seen[$path] = $now;
    $this->rebuild($path);
});

Maintain an ignore list as config, not code. vendor/, node_modules/, .git/, storage/, .idea/, and editor noise (*.swp, *~) should be filtered before they reach your handlers. Filtering after the event fires still costs you the wakeup; filter as early as the package permits, and keep the list in config/ so ops can adjust it without a deploy.

Pair with a rebuild step, not an in-place mutation. The pattern that works is watch → debounce → shell out to the real tool (npm run build, php artisan view:clear) rather than mutating state inside the callback. Keeps the handler fast, keeps failures isolated, and keeps the watcher from blocking on slow work while events queue up.

Log to a location outside the watched tree. A watcher that logs into a watched directory re-triggers itself. Obvious in hindsight, and responsible for more than one "why is my CPU at 100%" ticket.

Gotchas and Tips

ENOSPC is not a disk error. It means inotify watch descriptors are exhausted. The fix is sysctl fs.inotify.max_user_watches=524288 (and max_user_instances), but the symptom looks like a full disk, so put the fix in your runbook before someone hits it at 4:55pm on a Friday. Recursive watching of vendor/ exhausts the limit fast.

Docker Desktop bind mounts silently swallow events. inotify does not propagate reliably through the macOS/Windows → Linux VM layer. The watcher starts, reports no error, and never fires. Test in the exact container image you plan to run in, on the exact mount topology, before committing to the approach. If local dev is macOS and production is Linux, you need two mechanisms or a polling fallback — and that's a decision to make in a timeboxed spike, not mid-sprint.

The blocking start() loop and Ctrl+C. The loop runs forever; signal handling is on you. Register pcntl signal handlers (where available) to shut down cleanly, and if you run it under Supervisor or a systemd unit, set stopwaitsecs / TimeoutStopSec high enough that a stuck inotify read doesn't force a SIGKILL. For dev, Ctrl+C is fine — for anything long-lived, plan the shutdown path.

Check the composer constraints before you plan the upgrade. A package at this activity level may not declare PHP 8.x or Laravel 10/11 support. composer require will tell you immediately, but a hard conflict means either a fork with a loosened constraint or a different package. Find out in the spike, not on merge day.

Test doubles, not real watchers, in CI. Never spin up an actual inotify loop in a test suite — it's flaky, slow, and OS-dependent. Bind your contract to a fake that records watch()/on() calls and lets you invoke handlers by hand. The real loop gets a manual smoke test, or a single tagged test excluded from CI.

Extension points are what you'd expect and not much more. If you need custom filtering, debouncing, or event coalescing that the package doesn't offer, subclass or wrap — don't patch vendor/. With zero upstream activity, any local patch is a permanent fork you now maintain. Prefer the wrapper layer; it keeps the diff at zero when (not if) you replace the underlying package.

Pin the version and put it on the SBOM. BSD-2-Clause is friendly to proprietary code, but a 7-star package with no maintenance cadence should be pinned exactly and tracked. If it goes quiet upstream, you own the PHP-compatibility fixes — budget a small recurring line item for that, and name the owner before anything reaches production.

Have an exit story documented. The best property of this package is that it's easy to read, easy to fork, easy to remove. That's only true if you wrapped it. If domain code imports Cartalyst\ResourceWatcher\* directly, you've turned a weekend swap into a refactor. The wrapper is the whole point.

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.
elsayed85/laravel-api-exceptions
ouhssini/laravel-demo-mode
daworks/ncloud-outbound-mailer-driver
gp247/cms
ignibyte/aic-gates
nexa-merchant/blog
novasa/laravel-language-center
opencage/geocode
adolfoholzer/parameters
livenetworks/ln-starter
shitoudev/permissionmanager
johannesclimacus/laravel-tui
hocvt/seotools
vitalybaev/sypexgeo
amphibee/themosis-ignition
gito-me/fast-excel
frankyso/laravel-ipaymu
geolize-studio/laravel-chile
boomerangui/chat
cgv/localized-resource