Recently Matt Shaw from Delicious Brains published a post about a new library they created to help in one of their products. This library WP_Queue provides a Laravel-like Job management system for WordPress. A job queue is a system that allows you to schedule jobs to run in the future. We tend to use jobs for two reasons. First, we may need to wait awhile, like if we want to schedule a follow-up email in a week. The other reason is performance. Maybe we need to do something computationally expensive and don’t want the user to wait.
A job manager gives us a scheduling system — some way to store jobs until they need to be run — and a job runner — some tool for running the jobs. WordPress’ wp_cron sort of fits this description. However, through using WP_Queue, I’ve found it fits my needs better.
The WP_Queue package looks great for a few reaons. First, is that the jobs are abstracted from the runner and scheduler. I can write a job class and test it as unit in isolation. Secondly, the jobs scheduler is abstracted. By default, jobs are recorded in the WordPress database, and then when they are needed, they are scheduled with wp_cron. But I can also use a development driver that makes them synchronous and there is a Redis driver in progress. So let’s get started.
The WP_Queue library is a composer package. First, install the package in your plugin:
You will need to add the database tables for the scheduler. The Readme for the package has instructions. Add this to your plugin’s activation hook or wherever you add your own tables.
If you’ve ever used Laravel’s job queue, the structure of the \WP_Queue\Job class, will be familiar. Your job class has to have a handle() method. That method is called when the job is run. You probably will have a __construct() method as well.
The key concept to understand about these classes is that the properties of the class are serialized to the database when the job is scheduled and used to instantiate the class when it is run.
For example, let’s say you wanted to create a job to run whenever a post is saved. In the __construct() method, you would pass the post ID and use that argument to set a property. When the handle method runs, that property will be set with the post ID. In the handle method, you can use that property — the saved post ID that was saved with the job — to get the saved post from the database. Here is what that looks like:
Copy A Post’s JSON To A File When It Is Saved
What we’ve done so far is a job that gets a post from the database. That’s cool, we could start a runner, schedule this job to run every time a post is saved, and make this job get the saved post’s REST API response and write it to a JSON file. Let’s work through that list backward.
Why that order? That last requirement will be one class, which I can test in isolation, and if it works, then I can setup the runner and hook into the save_post action knowing that job works. If I did it the other way around, I wouldn’t know if my problem was the job, or the queue. That’s bad science.
Write A Post’s JSON To A File
Let’s keep working on our job class. We have everything we need to get the post. We need to get the response object that the WordPress REST API would create for the post, serialize it to json and write that JSON to a file.
I wrote a post for Torque awhile ago about how to get posts from the WordPress REST API without making HTTP requests. I stole most of this code from there:
This is the complete job. I used the WordPress REST API’s post control to create a WP_REST_Response and used json_encode() to make it a string and saved it to a file named for the post slug.
Where you stored the file and what you call it depends on your needs.
Right now, we can instantiate this class to test if it works. We can run it directly with something like this, where $postId was the ID of a published post:
Then you should see in the path you set for writing a file, a file with the JSON representation of the post. We can also write an integration test for it:
Scheduling The Job To Run When The Post Is Updated
Now that we know our job would work if it was scheduled to run, we need to schedule it to run, whenever a post is saved. The save_post action fires when a post is saved, so we can use that. In the callback function, we will instantiate the job class and pass it to wp_queue()->push().
That’s enough to schedule it to run, as soon as possible.
Delaying Job Execution
The second argument of the push() method is the delay time to run the job. The last code example didn’t use that argument. The job will run as soon as the queue gets to it. If we wanted to delay it by 5 minutes, we could pass 600 — 5 minutes in seconds — as the second argument.
Setup The Job Runner
Last step: we need to setup the queue to run. We can run the queue using wp_cron:
When you are doing local development, it’s better to use the “sync” driver, instead of the database driver so jobs run synchronously, IE right away. This makes testing easier.
Use Without WP-CRON
I’m not going to enumerate the many shortcomings of wp_cron. But, what’s really cool about WP_Queue is I can set up my own system to run it. For optimal perfomance, I don’t want to rely on WordPress to trigger wp_cron and I don’t want to add more “cron” jobs to its queue. Instead, I’d rather add a REST API endpoint and ping it with an external cron job, such as one run with setCronJob.com or something.
We can access the queue with the function wp_queue(). That returns a Queue object that has a public method called worker(). That returns a Worker object with a process() method that will run one job if any jobs are available to be processed. We can use a while loop to run jobs until process returns false, and use an incrementing integer to keep the loop from running for too many requests:
In this example, I’m hooking it up to a REST API route, that would allow an external cron service to trigger the queue. You manage your server, it would be most secure to trigger that function with a real cron or a command that can only be run from inside of the server/ container. If that’s not possible, at a public/secret key authentication check or something.