Monitoring for Filesystem Changes using PHP and Laravel

Let’s say you have a Laravel application that does some data processing, and you want to monitor a directory for incoming changes, that you can then process using queued jobs. There are a couple of ways you could do something like this.

You could scan those directories on a schedule using a cronjob. It’s doable. But what happens if you want to monitor a few thousand directories for changes? You can use tools like incron. Also doable, but another dependency.

But what if I told you you could do it all with PHP. And within Laravel, no less?

This is where the PHP inotify extension comes in. Be aware that this is not installed with the standard PHP installation. It’s part of PECL, the 2000s throwback. So, to get started, you will first need to install the extension:

$ pecl install inotify

And let pecl do it’s thing. Follow any instructions to install the extension and be sure it’s working. php -i | grep inotify is usually sufficient.

Now, in your Laravel app, create a new command. We’ll call it watch. Make it look something like this:

<?php

namespace App\Console\Commands;

use Illuminate\Console\Command;
use Log;

use Symfony\Component\Finder\Finder;

class Watch extends Command {
    protected $signature = 'watch';
    protected $description = 'Watches paths for changes.';

    private $mapping = [];
    private $io_instance = null;
    private $semaphore = true;

    public function handle() {
        $directories = ["/tmp/foo", "/tmp/bar"];

        if (!empty($directories)) {
            $this->io_instance = inotify_init();
            stream_set_blocking($this->io_instance, 0);

            foreach ($directories as $directory) {
                $this->add_watch($dir);
            }

            while ($this->semaphore) {
                sleep(1);
                $events = inotify_read($this->io_instance);

                if (!empty($events)) {
                    foreach ($events as $event) {
                        $dir = $this->mapping[$event['wd']];
                        $filename = $event['name'];
                        $path = "$dir/$filename";
                        Log::info("Found event for $path.");

                        $job = new MyJob();
                        $job->file = $path;
                        dispatch($job);
                    }
                }
            }

            Log::info("Shutting down gracefully.");
            foreach (array_keys($this->mapping) as $watch) {
                inotify_rm_watch($this->io_instance, $watch);
            }

            fclose($this->io_instance);
        }
    }

    private function add_watch($dir) {
        $watch = inotify_add_watch($this->io_instance, $dir, IN_CREATE | IN_DELETE | IN_MODIFY );
        $this->mapping[$watch] = $dir;
        Log::info("Watching " . $dir);
    }
}

Don’t forget to add your new command to the Kernel.php file. And you trigger your job just like you would run any other artisan command:

$ php artisan watch

Try moving some files in and watch it pick up the changes and dispatch the jobs. Pretty neat!

One thing to be aware of is that it will not watch subdirectories. You will need to add a separate inotify_add_watch for every directory you want monitored. I highly suggest pairing this with the Symfony Finder component as a great way to get an entire tree of directories, each of which can have an inotify_add_watch on them.

So now that you have a command, you just have to run it someway. And, just like with Laravel queues, you can run this using a monitoring daemon. I personally like supervisord. So you might add this config:

[program:laravel-app-watcher]
process_name=%(program_name)s_%(process_num)02d
command=php /path/to/installation/artisan watch
autostart=true
autorestart=true
user=laravel-app
numprocs=1

Stuff that into supervisord and restart, and you’re good to go!

Did something I wrote help you out?

That's great! I don't earn any money from this site - I run no ads, sell no products and participate in no affiliate programs. I do this solely because it's fun; I enjoy writing and sharing what I learn.

COVID-19 has taken the world by storm and left a lot of brokenness in its wake. A lot of people are suffering. If you feel so inclined, please make a donation to your local food bank or medical charity. Order take-out from your local Chinese restaurant. Help buy groceries for an unemployed friend. Help people make it through to the other side.

But if you found this article helpful and you really feel like donating to me specifically, you can do so below.

Read More

Using Phinx Programmatically

Phinx is a really cool database migration package that allows you to write changes to your database as code. It keeps track of which changes have been applied and allows you the option of rolling back if you hit an issue. All the documentation on Phinx describes a typical setup where you would run the phinx command to do your migrations. And that is all fine and good in most projects. But what happens if you are integrating Phinx into an existing project that already has a lot of the usual scaffolding in place?