Queues/Workers
Some tasks, like processing large CSV uploads, can slow down your app and hurt the user experience. Leaf makes it easy to offload heavy work to background jobs, keeping your app fast and responsive. With built-in queuing, you get better performance without the complexity.
Queues are only supported by Leaf MVC. We plan to add support for Leaf Core in the near future.
Leaf queues have three parts:
- the queue (stores jobs eg: database table, redis, ...)
- the job (task to run eg: sending emails)
- the worker (processes jobs eg: terminal process)
You write your tasks as jobs, dispatch them to a queue so they can be processed later, and run a worker to process the jobs in the background. That way, your app stays fast and responsive while handling heavy tasks in the background. This might sound complicated, but Leaf makes it super easy to get started.
Installation
To get started with Queues in Leaf, you need to install the leaf/queue
package:
leaf install queue
composer require leafs/queue
By default, Leaf MVC uses your database as the queue backend, storing jobs in a leaf_php_jobs
table. If you're fine with these defaults, just restart your server—Leaf will detect the queue setup and automatically start processing jobs alongside the PHP and Vite servers.
Creating a job
A job is a class containing any logic you might want to offload to the background. For example, you might want to send a welcome email after a user registers, but due to an external API call or overloaded email server, sending the email might take a few seconds. Instead of making the user wait, you can create a job to handle sending the email in the background.
You can create a new job using the g:job
command:
php leaf g:job SendEmail
This creates a SendEmailJob
class in app/jobs
with a handle()
method that runs when the job is processed. You can add any logic here, like using UserMailer to send an email:
<?php
namespace App\Jobs;
use Leaf\Job;
use App\Mailers\UserMailer;
class SendEmailJob extends Job
{
/**
* Handle the job.
* @return void
*/
public function handle($userId)
{
UserMailer::welcome($userId)->send();
}
}
Dispatching a job
Dispatching a job means adding it to the queue so it can be processed later. You can imagine your queue as a to-do list, and dispatching a job is like adding a new task to that list. Leaf allows you to dispatch jobs from anywhere in your application using the global dispatch()
function:
dispatch(SendEmailJob::class);
One major thing to note is that dispatching a job does not run it immediately. Instead, it adds the job to the queue, and a worker will process it in the background. This means your application can continue running without waiting for the job to finish.
Passing data to a job
Some jobs like the send email job above may require some data to be passed to the job, but this will need to be done from the place where you dispatch the job because the job itself should not be aware of where the data is coming from. All jobs run in a stateless environment, so you can't access the current request, user, session, or any other stateful data in your jobs. If you need to access the current request, you should pass the necessary data to the job as a parameter.
You can pass data to the job using the with()
method when dispatching it to the queue:
dispatch(SendEmailJob::with($userId));
You can also dispatch multiple jobs at once:
dispatch([
SendEmailJob::with($userId),
SendReminderJob::with($userId),
ScheduleFlightJob::with($userId),
]);
After dispatching the job, you need a worker to run all jobs in the queue.
Starting a worker
Workers are the final piece of the puzzle. A worker is a process that runs in the background and processes jobs from the queue. Without a worker running, your jobs will just sit in the queue without being processed. Leaf will automatically start a worker for you when you start the PHP server using leaf serve
or php leaf serve
. However, if you want to start a worker manually, you can use the queue:work
command:
php leaf queue:work
This is fine in development, but in production, you’ll need to set up your server to keep your workers running continuously. Check out this guide on deploying queues/workers for more information.
Specifying options for a job
There are times when you need a job to behave a specific way, for instance, you may want to delay a job for a few minutes or specify the number of times a job should be attempted. You can do that by directly setting the options on the job:
<?php
namespace App\Jobs;
use Leaf\Job;
use App\Mailers\UserMailer;
class SendEmailJob extends Job
{
protected $delay = 10; // add 10 sec delay //
protected $tries = 1; // try job only once (default 3) //
/**
* Handle the job.
* @return void
*/
public function handle($userId)
{
UserMailer::welcome($userId)->send();
}
}
The available options are:
Option | Description |
---|---|
delay | The number of seconds to wait before processing a job. |
delayBeforeRetry | The number of seconds to wait before retrying a job that has failed. |
expire | The number of seconds to wait before archiving a job that has not yet been processed. |
force | Whether to force the worker to process the job, even if it has expired or has reached its maximum number of retries. |
memory (WIP) | The maximum amount of memory the worker may consume. |
timeout | The number of seconds a child process can run before being killed. |
tries | The maximum number of times a job may be attempted. |
Scheduling jobs NEW
Some background tasks need to be run at specific times or intervals, for instance, every week, you get an email report of your app's activity. This is usually done using CRON jobs, but Leaf allows you to schedule jobs directly from your already existing jobs. Let's take an example of sending an application report to the admin every week. First, you create a job that sends the report:
<?php
namespace App\Jobs;
use Leaf\Job;
use App\Mailers\AdminMailer;
class SendAppReportJob extends Job
{
/**
* Handle the job.
* @return void
*/
public function handle()
{
AdminMailer::applicationReport()->send();
}
}
This is an ordinary job that needs manual dispatching, meaning you would need to call dispatch(SendAppReportJob::class)
somewhere in your app. However, since this is a job that needs to be run automatically, you can add a schedule()
method to the job that defines when the job should be run:
<?php
namespace App\Jobs;
use Leaf\Job;
use App\Mailers\AdminMailer;
class SendAppReportJob extends Job
{
/**
* Handle the job.
* @return void
*/
public function handle()
{
AdminMailer::applicationReport()->send();
}
/**
* Define the job schedule.
* @return \Leaf\Job
*/
public function schedule()
{
return $this->every('week')->on('tuesday')->at('8:00'); // every week on tuesday at 8am
}
}
Leaf will automatically detect the schedule()
method and run the job at the specified time. You don't need to do anything else. Just make sure your worker is running, and Leaf will take care of the rest.
While this human-readable syntax is great for most use cases, you can also use CRON expressions if you need more control over the scheduling:
public function schedule()
{
return $this->cron('0 8 * * 2'); // every week on tuesday at 8am
}
Limitations of Queues/Workers
Just as with every other aspect of Leaf, we try to set everything up for you so you can get started right after running leaf install
. This makes Leaf's queue system very easy to use, but it also comes with some limitations:
- Queues are completely stateless. This means that you can't access the current request, user, session, or any other stateful data in your jobs. If you need to access the current request, you should pass the necessary data to the job as a parameter.
- Due to its simplicity, Leaf's queue system is not as feature-rich as other queue systems like Laravel's. We are working on adding more features to the queue system.
Switching to Redis
Leaf's queue system uses the database as the default queue backend. If you want to use Redis instead, you can change the default queue connection in your .env
file:
QUEUE_CONNECTION=redis
This will switch to using your Redis connection for the queue. Leaf Redis currently only supports one connection, but we are working on adding support for multiple connections.
Configuration
If you want to change the default config for the queues and workers, you need to publish the queue config file using the MVC console:
php leaf config:publish queue
This will generate a queue.php
file in your config
directory which looks something like this:
<?php
return [
/*
|--------------------------------------------------------------------------
| Default Queue Connection
|--------------------------------------------------------------------------
|
| Here you may specify which of the queue connections below you wish
| to use as your default connection for all queue work.
|
*/
'default' => _env('QUEUE_CONNECTION', 'database'),
/*
|--------------------------------------------------------------------------
| Queue Connections
|--------------------------------------------------------------------------
|
| Here you may configure the connection options for every queue backend
| used by your application. An example configuration is provided for
| each backend supported by Leaf. You're also free to add more.
|
| Drivers: "redis", "database", "file (BETA)"
|
*/
'connections' => [
'redis' => [
'driver' => 'redis',
'connection' => _env('REDIS_QUEUE_CONNECTION', 'default'),
'table' => _env('REDIS_QUEUE', 'leaf_php_jobs'),
'schedules.table' => _env('REDIS_QUEUE_SCHEDULES_TABLE', 'leaf_php_schedules'),
],
'database' => [
'driver' => 'database',
'connection' => _env('DB_QUEUE_CONNECTION', 'default'),
'table' => _env('DB_QUEUE_TABLE', 'leaf_php_jobs'),
'schedules.table' => _env('DB_QUEUE_SCHEDULES_TABLE', 'leaf_php_schedules'),
],
],
];
You can set up multiple queue connections, and Leaf will connect to them when a job is run. For now, only database queues are supported, but we are working on redis and file drivers which will be available pretty soon.
Using a different connection
Once you have configured another database connection, you can run a job using that queue connection by specifying it inside the job you create like this:
<?php
namespace App\Jobs;
use Leaf\Job;
use App\Mailers\UserMailer;
class SendEmailJob extends Job
{
protected $connection = 'myOtherConnection';
/**
* Handle the job.
* @return void
*/
public function handle($userId)
{
UserMailer::welcome($userId)->send();
}
}
From there, you can dispatch the job just as you always do:
dispatch(SendEmailJob::with($userId));