PHP executes code in series, which means one thing is done after another. This can be problematic when you need to do a lot of processing in one session or if you are relying on calls to external APIs. In this article, I’m going to show you how to use asynchronous PHP in WordPress to address this issue.
It isn’t uncommon for a server to be configured with a 30-second timeout limit for each request, which puts a hard cap on the amount of time each session has to do its work. Of course, if that processing is required in order to complete a front-end request, the end user is unlikely to stick around for 30 seconds.
I recently had a situation where the requirements I had to address could not be handled in one request. For this, my client used a lead form to trigger three external API requests, one of which was to a service that was slow and unreliable.
In addition, they wanted to get the site visitor to the thank you page as quickly as possible. Because I knew I had to wait for the API request, there was going to either be a thank you page that takes 20 seconds to load or a timeout.
Since the unreliability of the remote API made it necessary to save the data in WordPress so it could be re-sent if needed and to log the status of the API requests, the amount of work needed couldn’t be done within the 30-second session. There was also no way we could save the data, make the requests, update the status of those requests, and then redirect the user to the thank you page before they became frustrated by the slow page-load time.
The answer was to save the data, schedule tasks with an asynchronous task manager, and then redirect the user. Then, each of the three API requests would run in their own individual session, have their own 30 seconds to complete, and not impact user experience. I probably could have moved saving the data into an asynchronous task as well — but it wasn’t necessary.
There are several asynchronous task managers specifically designed for WordPress, but one of the best and easiest to use is wp-async-task, a task manager open sourced by TechCrunch and developed by 10up.
How It Works
To use the wp-async-task, you need to use an action hook. This could be with a hook in core, another plugin, or via do_action(). Normally, you would hook directly to that hook in order to do some process, in series, when that hook happens.
With wp-async, the processing on that hook is deferred to a later session. It’s actually fairly simple to do. We need the hook, a class with a property, and two methods to instantiate that class. And then we need to hook into a hook generated by that hook. The task manager makes a new POST request to WordPress and then passes data from the original hook to your new hook in the second session — which is not something you can normally do.
The Problem: All At Once Or Nothing
In the next section, I will walk you through the process. But first, let’s look at the kind of code we would be modifying to work with this:
add_action( 'save_post', 'josh_send_to_api' ); function josh_send_to_api( $id ) { $thing = get_post_meta( $id, 'something', true ); $r = wp_safe_remote_post( add_query_arg( 'id', $thing, 'http://apiexample.com/' ) ); if ( ! is_wp_error( $r ) ) { $body = json_decode( wp_remote_retirve_body( $r ) ) ); if ( isset( $body->key ) ) { update_post_meta( $id, 'api_response', $body->key ); } else { update_post_meta( $id, 'api_response', 'none' ); } } }
The problem with this code is that the process of saving a post will not complete until the request to the remote API is complete. If the request to the remote API takes longer than the server’s time out, then it will not complete.
Saving the post, sending data to the remote API, and recording the result as post meta does not have to be one discrete process. Instead, we can let the post save as normal, and then, in a second PHP session, retrieve that data and record the response as post meta.
Setting It Up
Using a wp-async-task is pretty straight forward, as all of the heavy lifting is handled by the library itself. Implementing the library requires a class that extends the class WP_Async_Task and does three simple things.
This class should have a protected property action, which is the name of the action this asynchronous task uses. In this case, it will be “save_post.” I should note that, in my experience, I’ve found that hooking multiple tasks to one hook is unreliable. Instead, I’ve created three hooks that run one after another and use one for each of my three API requests.
To keep things simple let’s stick to “save_post.” Let’s start the class with that:
class Josh_Task extends WP_Async_Task { /** * Action to use to trigger this task * * @var string */ protected $action = 'save_post'; }
The next thing we need is a protected method called “prepare_data,” which we will use to prepare the data. The important thing to know about this method is that it runs during the session that triggers the asynchronous task, not the one that processes it. That is important to keep in mind, as all of the globals and superglobals of the current session are available for you as data that will be passed to the next session.
This method will be passed an array of data containing all of the parameters of the hook. If you use a hook that exposes three parameters, the first one will be in key zero, the second in key one, and so on. In this example, we just need to take the first parameter from save_post and the post ID and send it to the next session.
The prepare_data method forms the POST data for the session that executes the asynchronous task. Anything we want in that session must be returned at prepare_data or stored in the database.
Here is the updated class to send the post ID:
class Josh_Task extends WP_Async_Task { /** * Action to use to trigger this task * * @var string */ protected $action = 'save_post'; /** * Prepare POST data to send to session that processes the task * * @param array $data Params from hook * * @return array */ protected function prepare_data($data){ return array( 'post_id' => $data[0] ); } }
The third and final method of this class, “run_action” is a protected function that is used to actually run the task. This method is executed in a different session than prepare_data. As a result, inside of it, you must use the POST superglobal to retrieve the data you need to run the task. Luckily, the POST data is what you set up in prepare_data.
In this method, we validate that post_id in POST and is set and is an integer and if so use it to fire another action. The wp-async-task’s authors suggest using wp_async_ as a prefix.
class Josh_Task extends WP_Async_Task { /** * Action to use to trigger this task * * @var string */ protected $action = 'save_post'; /** * Prepare POST data to send to session that processes the task * * @param array $data Params from hook * * @return array */ protected function prepare_data($data){ return array( 'post_id' => $data[0] ); } /** * Run the asynchronous task * * Calls send_to_api() */ protected function run_action() { if( isset( $_POST[ 'post_id' ] ) && 0 < absint( $_POST[ 'post_id' ] ) ){ do_action( "wp_async_$this->action", $_POST[ 'post_id' ], get_post( $_POST[ 'post_id' ] ) ); } } }
Wiring It Up
Now that the class is in place, we can pull this all together in a few simple steps. The first step is at plugins_loaded or later to instantiate the class. Obviously, the wp-async-task library must be present and included before this point. I recommend installing it via composer. You can also install it as a plugin, which I think is a very bad idea as plugins can be deactivated.
The second thing we need to is hook our new action, which is fired inside the class to our original callback. Here is how that looks:
add_action( 'wp_async_save_post', 'josh_send_to_api' ); function josh_send_to_api( $id ) { $thing = get_post_meta( $id, 'something', true ); $r = wp_safe_remote_post( add_query_arg( 'id', $thing, 'http://apiexample.com/' ) ); if ( ! is_wp_error( $r ) ) { $body = json_decode( wp_remote_retirve_body( $r ) ) ); if ( isset( $body->key ) ) { update_post_meta( $id, 'api_response', $body->key ); } else { update_post_meta( $id, 'api_response', 'none' ); } } }
More Asynchronous
I hope this article has helps you understand why you may need to use PHP in an asynchronous manner. With the practical example, you should now understand how to do it using a regular WordPress site.
Getting your feet wet with asynchronous PHP is important, even if it’s using two sessions to asynchronous PHP, because PHP7 will support asynchronous PHP.
9 Comments