Cronless queue:work in Laravel executed in background
Tags: laravel, webdev, cron, php
Published: 2024-06-30 23:28:56

There are some approaches how to execute `queue:work` but I found them useless. Here is a **solution for most shared hostings** that:
- does not require additional route
- does not require remotely visiting website
- does not require shell access
- does not require cron access
- requires `bash` with `flock` on server (for single execution protection)
- requires PHP `exec` function available
- requires shell `php` command (`php-cli` installed on server)
- **runs in the background so makes no website slowdowns**
- can be set to **execute not more than once between minimum time span** of $runEverySec seconds

## 1. Let's create new Middleware:

```bash
$ php artisan make:middleware QueueWorkMiddleware
```

## 2. Use it as global middleware in `bootstrap/app.php`:

```php
...
    ->withMiddleware(function (Middleware $middleware) {
        $middleware->append(App\Http\Middleware\QueueWorkMiddleware::class);
    })
...
```

## 3. Put contents to `App/Middleware/QueueWorkMiddleware.php`:
```php
<?php

namespace App\Http\Middleware;

use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;


class QueueWorkMiddleware
{

    // lock file that prevents too many executions
    public $lockFile = 'queue.lock';
    // log from queue command
    public $logFile = 'queue.log';
    // log from background exec command
    public $execLogFile = 'exec.log';
    // pid of executed command
    public $pidFile = 'queue.pid';
    // php command path
    public $phpExec = '/usr/bin/php';
    // queue:work command
    public $queueCmd = 'artisan queue:work --stop-when-empty';
    // minimum time in seconds between executions
    public $runEverySec = 10;
    
    /**
     * Handle an incoming request.
     *
     * @param  \Closure(\Illuminate\Http\Request): (\Symfony\Component\HttpFoundation\Response)  $next
     */
    public function handle(Request $request, Closure $next): Response
    {
        // get pid and lock file names
        $pidFile = base_path()."/".$this->pidFile;
        $lockFile = base_path()."/".$this->lockFile;
        
        if( 

            // if there is no lock file and
            !file_exists($lockFile) && 

            // there is no pid file (queue was never executed before)
            // or time between pidfile modification is more or equal $runEverySec
            (!file_exists($pidFile) || (time()-filemtime(base_path()."/{$this->pidFile}") >= $this->runEverySec))
          ) 
          { 
            // do the work
            $this->work(); 
          }

        return $next($request);
    }


    public function work() {     
        // file names
        $basePath = base_path();
        $lockFile = "{$basePath}/{$this->lockFile}";
        $logFile = "{$basePath}/{$this->logFile}";
        $execLogFile = "{$basePath}/{$this->execLogFile}";
        $pidFile = base_path()."/".$this->pidFile;

        // main queue command and lock file removal
        $cmd = "{ {$this->phpExec} {$this->queueCmd} > {$logFile} 2>&1; rm {$lockFile}; }";

        // go to base path and run command by flock (this guarantees single execution only!)
        $cmd = "cd {$basePath} && flock -n {$lockFile} --command '{$cmd}'";
        
        // execute command in background
        exec(sprintf("%s > {$execLogFile} 2>&1 & echo $! >> %s", $cmd, $pidFile));

        return true;
    }

}
```