The “standard” way of processing AJAX requests in WordPress involves using WordPress’ admin AJAX API — whether you’re actually working in the admin or not. For front-end use of AJAX, especially on high-traffic sites, repurposing the admin AJAX API is not a great option.
The 10up Engineering Best Practices provides a good breakdown as to why using admin AJAX for the front-end is not a good idea. It points out that using WordPress’ admin AJAX API causes the WordPress admin to be fully bootstrapped and has no built-in caching.
For its intended purpose — running the WordPress backend — this makes perfect sense, however that’s not what this article is about. This article shows how to create a custom, internal API for processing AJAX requests in your WordPress project. Beyond performance issues, this strategy is designed for maximizing the reusability of code and for the reduction of redundancy.
It’s important to mention that this is definitely an advanced tutorial and thus it’s not for everyone. Part of my reason for saying that is because this is not a “copy, paste, and go” type tutorial. You really need to understand what’s going on, as you will need to modify many parts of this to fit your needs.
Before we begin
The code examples provided below all use namespaces. I am using the namespace \josh\api\internal, but you will want to change that — at least the root namespace — to match your specific project. I am also assuming that you have an autoloader in place to load these classes. If not, you can include them manually.
I am aware that this creates a backwards compatibility problem with PHP 5.2. Personally I don’t care, as this is something I use in both client projects and my personal projects, where I get to specify the PHP version used. If you want to implement this in a plugin for release, you will need to make the appropriate modifications or decide not to support obsolete versions of PHP.
Why not use the WordPress REST API?
Thanks to the WordPress REST API, creating an API for your WordPress site to handle basic create/read/update/delete (CRUD) operations is unnecessary, because it’s very easy to perform these operations via jQuery AJAX.
One thing to keep in mind is that the core REST API is never going to be the answer to all of your needs. I’ve used the methodology I’m detailing in this article for everything from processing forms, to performing partial page reloads, to sending data to the browser to be rendered with Handlebars.js.
That said, I fully intend to revisit this approach once the next iteration of the core REST API is complete. My goal will be to see if it makes sense to use some of the classes in the REST API for routing, dispatching, or securing requests. I will follow up here on Torque, or on my own site, depending on what I find.
Creating a workflow to prevent redundancy
One of the great things about creating an internal API for processing AJAX requests is that it allows for a dramatic reduction in code redundancy. When using the standard admin AJAX API, I find it very annoying to have to do security checks, caching, responding, and error handling within each callback. When I create my own AJAX API, I write one method for each of the steps, and every request passes through them.
In the workflow I’m showing here, I simply add a new class, which is named for the action in the request URL, and that action is now part of the API. Most of the time, these “action classes” are very simple. The rest of the process is handled automatically. There is no cutting and pasting code for each of the other steps, and when I make a change to the process, I only make it once.
This system has 3 main classes: One for adding endpoints, one for routing requests, and one for saving. The routing class calls the action class to handle the actual processing of the request. These classes are designed to accept the sanitized POST or GET data, and will only be called when an action is considered valid and the data passes sanitation.
Adding endpoints to WordPress
The first class we will need to create is one that will add the endpoint and tag rewrite rules. It will also add the template redirect filter that goes in the routing class. I add it here since this is the only one of the three classes that isn’t static, and therefore I can use a __construct() magic method to add it.
Of course, it makes more sense to use a separate plugin API manager than __construct() for adding hooks. I use one that is almost identical to what Carl Alexander shows in this article. For simplicity, however, in this article I’ll stick to using the __construct() magic method.
The whole class is shown below. It’s very simple. It just has the __construct() and one method to add a rewrite tag and rewrite rule, which we will need for sending the AJAX requests. Once this is in place, you probably want to use wp_localize_script() to output the URL for the endpoint, just like you would use the URL for admin AJAX.
<?php namespace josh\api\internal; /** * Class endpoints * * Adds enpoints for internal API * * @package josh\api\internal */ class endpoints { function __construct() { add_action( 'init', array( $this, 'add_endpoints' ) ); add_action( 'template_redirect', array( route::init(), 'do_api' ) ); } /** * Add endpoints for the API */ function add_endpoints() { //add "action as a rewrite tag add_rewrite_tag( '%action%', '^[a-z0-9_\-]+
Now we can send requests to a url like example.com/slug-internal-api?action=example, and will be able to route to the action set in the action GET variable. Be sure to prefix your endpoint with your site or plugin’s API slug.
Routing and dispatching requests
As I said before, one of the points of this system is to have as much of the request as possible pass through exactly the same code to handle everything besides the saving. The next class, called route, will handle getting the sanitized data to the right place and returning the right response, properly formatted, and with the right headers.
In order for this system to work, each class for processing will need to be in a specific location and include three specific methods. I put these classes in a namespace under the API called “action,” and used an interface called action for each one. This interface will require that the classes that inherit it have method names: “act,” “args,” and “method.” I will detail what each one does as I go through the router and give an example of a class in the “Processing Requests” section below.
Using an interface assures that the classes have the three right methods. It also allows for an additional validation step. One of the things that you must avoid with a system like this is creating the ability for any arbitrary class to be called, as that would present a serious security risk. One of the ways I prevent this from happening is by ensuring that the class that gets called is in a specific namespace and implements the interface.
The main logic of this class is housed in the do_api() method. This method gets the action query var if the current request is to our endpoint. If so, it gets the name of the class to use for processing. If that class exists, it uses the authorization class — which I will discuss in the next step — to check if the request can be authorized. If so, it proceeds to get the correct data and then sanitize it. It will then process it and return the response, or an error code if something went wrong.
Here is what it looks like:
<?php public static function do_api() { global $wp_query; //get action, and if set, possibly act $action = $wp_query->get( 'action' ); //be sure to update "slug" to your project's slug if ( $action && strpos( $_SERVER[ 'REQUEST_URI'], 'slug-internal-api' ) ) { //get class to process with and proceed or return 501 if its invalid $action_class = self::action_class( $action ); $auth_error = $response = false; if ( $action_class ) { if ( false == auth::check() ) { $auth_error = true; }else { $params = self::get_args( $action_class::args(), $action_class::method() ); if ( is_array( $params ) ) { $response = $action_class::act( $params ); } else { $auth_error = true; } } } else { $auth_error = true; } if ( ! $auth_error ) { return self::respond( $response, 200 ); } else { return self::respond( false, 401 ); } } }
As you can see, each step in the process acts both to get data needed to continue the process and to validate the request. The most important validation step is the method action_class(). This method returns the namespaced path to the class, if it exists and implements the action’s interface. If not, it returns false. Here is what it looks like:
<?php protected static function action_class( $action ) { //define namespacing $namespace = __NAMESPACE__ . '\\actions\\'; //get namespace class name $class = $namespace . $action; //return class name if it exists and implements the action interface if ( class_exists( $class ) && $namespace . 'action' == class_implements( $class, false ) ) { return $class; } }
You may need to adjust the namespaceing to match how you configure this. If you are not using namespacing and an autoloader as an extra validation step, you should make sure the class you are calling here is in the right directory.
If action_class() returns a class name and the previous authorization step is passed, the router class will use its get_args() method to get the arguments to pass into the action class. This step takes the array of arguments set in the action class’s args() method and the HTTP method to be used from the method called method() and uses them to get the arguments expected by the action classes’ act() method.
The two methods involved are shown in the complete class code example linked below. They are called get_args() and sanitize(). I will not go into detail about them because the former simply determines if it should pass $_GET or $_POST into sanitize() with a simple switch. And the latter method() is really just a placeholder for your own sanitization.
How you sanitize is really something that depends on your specific data. In the example I show, I used wp_kses() for all data that isn’t an integer. Using wp_kses() for every field is most likely overkill and will not be performant.
Data sanitization is a huge topic that I can’t cover here. Depending on your needs, you may be able to use a simple loop like I did or you may need more conditional logic based on the current action.
Once the data is sanitized, the parameters are passed to the action class act() method and its value is passed to the respond() method of the class() — which is something that I will discuss below.
Here is the full code for the class. It’s heavily commented and full of notes on what you should change to match your needs:
<?php namespace josh\internal\api; /** * Class route * * Dispatch and respond to requests to internal API * * @package josh\api\internal */ class route { /** * Main router for internal API. * * Checks permission, and dispatches and returns, or returns error. * */ public static function do_api() { global $wp_query; //get action, and if set, possibly act $action = $wp_query->get( 'action' ); //be sure to update "slug" to your project's slug if ( $action && strpos( $_SERVER[ 'REQUEST_URI'], 'slug-internal-api' ) ) { //get class to process with and proceed or return 501 if its invalid $action_class = self::action_class( $action ); $auth_error = $response = false; if ( $action_class ) { if ( false == auth::check() ) { $auth_error = true; }else { $params = self::get_args( $action_class::args(), $action_class::method() ); if ( is_array( $params ) ) { $response = $action_class::act( $params ); } else { $auth_error = true; } } } else { $auth_error = true; } if ( ! $auth_error ) { return self::respond( $response, 200 ); } else { return self::respond( false, 401 ); } } } /** * Get a static class object, by action. * * Does not check if class exists. Use only for those allowed by self::action_allowed() * * @access protected * * @param string $action Action name. * * @param $action * * @return object The class object. */ protected static function action_class( $action ) { //define namespacing $namespace = __NAMESPACE__ . '\\actions\\'; //get namespace class name $class = $namespace . $action; //return class name if it exists and implements the action interface if ( class_exists( $class ) && $namespace . 'action' == class_implements( $class, false ) ) { return $class; } } /** * Returned an array of the specified args from GET or POST data * * @param array $accept_args Arguments to allow * @param string $method HTTP method to use GET|POST * * @access protected * * @return bool|array */ protected function get_args( $accept_args, $method = 'GET') { $method = strtoupper( $method ); switch ( $method ) { case "GET": $input = $_GET; break; case "POST": $input = $_POST; break; default: return false; } return self::sanitize( $input, $accept_args ); } /** * Sanitize incoming POST or GET var for accepted args. * * @param array $input The GET or POST data * @param array $accept_args Array of args to sanitize and return. * * @access protected * * @return bool|array */ protected static function sanitize( $input, $accept_args ) { $output = false; foreach ( $input as $key => $val ) { if ( in_array( $key, $accept_args ) ) { //return if its a number if ( is_int( $val ) || is_float( $val ) || empty( $val ) ) { $output[ $key ] = $val; } /** * IMPORTANT wp_kses() might be the wrong sanization function to use here. Use what makes most sense for you. */ /** * Filter to set allowed tags to use for wp_kses * * Will not run on nonallowed keys * * @param array $allowed_tags Defaults to value of wp_kses_allowed_html( 'post' ) * @param array $input Array of data being filtered. WARNING: Not sanatized! * @param string $key Current key */ $allowed_tags = apply_filters( 'slug_internal_api_allowed_tags', wp_kses_allowed_html( 'post' ), $input, $key ); $value = wp_kses( $input, $allowed_tags ); if ( $value ) { $output[ $key ] = $value; } } } return $output; } /** * Send the response * * @access protected * * @since 0.1.0 * * @param string|array|integer $response Response to send. Will be encoded as JSON if is array. If is integer and greater than 1, * @param int|null $status_code Optional. Status code to set for the response. if is the default of null, and not set by $response then success code 200 is used. * * @return string */ protected static function respond( $response, $status_code = null ) { if ( empty( $response ) ) { $status_code = 204; } if ( is_int( $response ) && $response > 1 ) { $response = false; $status_code = $response; } if ( ! is_null( $status_code ) ) { $status_code = 200; } status_header( $status_code ); if ( is_array( $response ) ) { wp_send_json_success( $response ); die(); } else{ echo $response; die(); } } /** * Holds the instance of this class. * * @access private * @var object */ private static $instance; /** * Returns an instance of this class. * * @access public * * @return route|object */ public static function init() { if ( ! self::$instance ) { self::$instance = new self; } return self::$instance; } }
Processing requests
In the previous section I touched on what I call the “action class.” This is the class that processes the action. I’ve already covered the three methods — act(), args(), and method() — which this class must have. Also, I have explained why I use an interface to ensure that those methods are set.
Just because the class has to have those three methods does not mean it can’t have more. Sometimes the method act(), which does the actual processing may call additional options.
I did want to provide a basic example of an action class. This one shows updating some meta fields for a post. Part of why I’m showing this is because if you look at the class in isolation, you will see some obvious issues. It’s essentially taking GET or POST data and passing it directly into update_post_meta(), which could be an issue for many reasons.
Because of the work that the router class did, I know that this class is only going to have the right keys in the params, and they are already sanitized properly. If any of the validation or sanitization steps failed, the act() method is never called. The fact that all of this logic is shared between all of my action classes makes the whole thing easy and manageable:
<?php namespace josh\internal\api; /** * Class example * * Example "action class" for illustrating how this might be used to save some data * * @package josh\internal\api */ class example implements action { /** * Save the data * * @param array $params An array of params, defined by self::args() * * @return bool Will be true if updated. False if it couldn't update or post ID doesn't exist. */ public static function act( $params ) { //start response as false $response = false; //get ID and check if post exists $id = $params[ 'id' ]; $post = get_post( $id ); if ( is_object( $post ) ) { //save 'example' post meta key as the other params unset( $params[ 'id' ] ); $response = update_post_meta( $id, 'example', $params ); } return $response; } /** * Params for this route * * Used to define what keys are in the array passed to self::act() * * @return array */ public static function args() { return array( 'frogs', 'toads', 'pirates', 'id' ); } /** * Define the HTTP method this action uses * * @return string */ public static function method() { return 'POST'; } }
Authorization
What you do in this class really depends on your needs and may even be completely unnecessary. Below you will find an example that just checks the validity of a nonce. This, along with properly configured Cross-Origin Domain Headers (CORS), may be all you really need.
Depending on what type of requests you are handling, you may want to verify a request’s origin. You may also want to check if the current user has a specific capability.
One thing I’m not doing in this example code, but have done in the past, is use the current action as a parameter of the check() method. This allows for different authentication strategies based on the current action. Here’s a basic example of how the auth class would work with a simple nonce check:
<?php namespace josh\internal\api; /** * Class auth * * Check if action is authorized * * @package josh\internal\api */ class auth { /** * Do the authorization checks. * * @return bool */ public static function check() { if ( wp_verify_nonce( '_wpnonce' ) ) { return true; } } }
Obviously, calling a class just to run one function doesn’t make a ton of sense. This class is a starting point for you to build in your own authentication system. For example, I recently did a project where I used my internal API for a complex multi-step form. I used the auth class to ensure that the person submitting the form was the “owner” of the current form entry and was using it in the right order.
Responding to requests
Earlier I showed the full example code for the router class. I included (but didn’t discuss) the respond() method. Depending on the range of responses you intend to employ, it may be enough. You might need to build a more complicated class for ensuring your responses are correct.
The respond() method I show simply ensures that it has the right status code, sends a status code header and the response back to the browser. I’m only accounting for the possibility that responses could be true or false or an array that should be returned as JSON.
What about caching?
Part of the beauty of this system it you essentially create a bottleneck which each request passes through. This makes it very easy to introduce caching. Here is a fairly simple modification of the the do_api() method, which uses the WordPress transient API to cache results.
<?php public static function do_api() { global $wp_query; //get action, and if set, possibly act $action = $wp_query->get( 'action' ); if ( $action && strpos( $_SERVER['REQUEST_URI'], 'slug-internal-api' ) ) { //get class to process with and proceed or return 501 if its invalid $action_class = self::action_class( $action ); $auth_error = $response = false; if ( $action_class ) { if ( false == auth::check() ) { $auth_error = true; } else { if ( is_array( $params ) ) { $cache_key = array_merge( $action_class::params(), array_keys( $action_class::params() ) ); $cache_key = md5( $action . implode( $cache_key ) ); if ( 'GET' === $action_class::method() && false == ( $response = get_transient( $cache_key ) ) ) { $params = self::get_args( $action_class::args(), $action_class::method() ); $response = $action_class::act( $params ); if ( 'GET' === $action_class::method() ) { transient_set( $cache_key, $response, HOUR_IN_SECONDS ); } } } else { $auth_error = true; } } } else { $auth_error = true; } if ( ! $auth_error ) { return self::respond( $response, 200 ); } else { return self::respond( false, 401 ); } } }
As you can see, the parameters used for the action class, and there values, are used to create a key for the transients. Also, it does not cache on POST requests, since that HTTP method should be used for modifying data. This could present an issue if you have actions without anything in the GET value. You may need to conditionally skip caching based on the current action.
Also, you may wish to use object caching instead. Of course, this only makes sense if you have a persistent object caching system in place. You should make your decision on which caching system you use or don’t use based on performance benchmarks. Caching involves writing additional data to the database or working memory, and you need to make sure that it is worth it.
Now make it your own
That’s the basic outline of how you would configure an internal API for handling high-performance AJAX request processing. As I said at the beginning of this article, this isn’t something you can just paste in and use. It will take some work to get it to match your specific needs and to ensure that it is performant on your server.
The good thing about this type of system is that it makes work easier, as you really only have to do it once. Also, if anything changes, you only need to change it in one place, instead of in all of the places that you hooked into WordPress’ admin AJAX. This, alone, makes it worth it, as less redundant code always equals better code, easier troubleshooting, and fewer mistakes.
Josh Pollock started learning WordPress development when he was supposed to be working on his masters thesis, which ended up being about open source solutions for sustainable design and was presented in a WordPress site. You can learn more about him at JoshPress.net or follow him on twitter @Josh412
); //add the endpoint //be sure to change "slug" to your unique slug" add_rewrite_rule( 'slug-internal-api/^[a-z0-9_\-]+$/?', 'index.php?action=$matches[1]', 'top' ); } }
Now we can send requests to a url like example.com/slug-internal-api?action=example, and will be able to route to the action set in the action GET variable. Be sure to prefix your endpoint with your site or plugin’s API slug.
Routing and dispatching requests
As I said before, one of the points of this system is to have as much of the request as possible pass through exactly the same code to handle everything besides the saving. The next class, called route, will handle getting the sanitized data to the right place and returning the right response, properly formatted, and with the right headers.
In order for this system to work, each class for processing will need to be in a specific location and include three specific methods. I put these classes in a namespace under the API called “action,” and used an interface called action for each one. This interface will require that the classes that inherit it have method names: “act,” “args,” and “method.” I will detail what each one does as I go through the router and give an example of a class in the “Processing Requests” section below.
Using an interface assures that the classes have the three right methods. It also allows for an additional validation step. One of the things that you must avoid with a system like this is creating the ability for any arbitrary class to be called, as that would present a serious security risk. One of the ways I prevent this from happening is by ensuring that the class that gets called is in a specific namespace and implements the interface.
The main logic of this class is housed in the do_api() method. This method gets the action query var if the current request is to our endpoint. If so, it gets the name of the class to use for processing. If that class exists, it uses the authorization class — which I will discuss in the next step — to check if the request can be authorized. If so, it proceeds to get the correct data and then sanitize it. It will then process it and return the response, or an error code if something went wrong.
Here is what it looks like:
As you can see, each step in the process acts both to get data needed to continue the process and to validate the request. The most important validation step is the method action_class(). This method returns the namespaced path to the class, if it exists and implements the action’s interface. If not, it returns false. Here is what it looks like:
You may need to adjust the namespaceing to match how you configure this. If you are not using namespacing and an autoloader as an extra validation step, you should make sure the class you are calling here is in the right directory.
If action_class() returns a class name and the previous authorization step is passed, the router class will use its get_args() method to get the arguments to pass into the action class. This step takes the array of arguments set in the action class’s args() method and the HTTP method to be used from the method called method() and uses them to get the arguments expected by the action classes’ act() method.
The two methods involved are shown in the complete class code example linked below. They are called get_args() and sanitize(). I will not go into detail about them because the former simply determines if it should pass $_GET or $_POST into sanitize() with a simple switch. And the latter method() is really just a placeholder for your own sanitization.
How you sanitize is really something that depends on your specific data. In the example I show, I used wp_kses() for all data that isn’t an integer. Using wp_kses() for every field is most likely overkill and will not be performant.
Data sanitization is a huge topic that I can’t cover here. Depending on your needs, you may be able to use a simple loop like I did or you may need more conditional logic based on the current action.
Once the data is sanitized, the parameters are passed to the action class act() method and its value is passed to the respond() method of the class() — which is something that I will discuss below.
Here is the full code for the class. It’s heavily commented and full of notes on what you should change to match your needs:
Processing requests
In the previous section I touched on what I call the “action class.” This is the class that processes the action. I’ve already covered the three methods — act(), args(), and method() — which this class must have. Also, I have explained why I use an interface to ensure that those methods are set.
Just because the class has to have those three methods does not mean it can’t have more. Sometimes the method act(), which does the actual processing may call additional options.
I did want to provide a basic example of an action class. This one shows updating some meta fields for a post. Part of why I’m showing this is because if you look at the class in isolation, you will see some obvious issues. It’s essentially taking GET or POST data and passing it directly into update_post_meta(), which could be an issue for many reasons.
Because of the work that the router class did, I know that this class is only going to have the right keys in the params, and they are already sanitized properly. If any of the validation or sanitization steps failed, the act() method is never called. The fact that all of this logic is shared between all of my action classes makes the whole thing easy and manageable:
Authorization
What you do in this class really depends on your needs and may even be completely unnecessary. Below you will find an example that just checks the validity of a nonce. This, along with properly configured Cross-Origin Domain Headers (CORS), may be all you really need.
Depending on what type of requests you are handling, you may want to verify a request’s origin. You may also want to check if the current user has a specific capability.
One thing I’m not doing in this example code, but have done in the past, is use the current action as a parameter of the check() method. This allows for different authentication strategies based on the current action. Here’s a basic example of how the auth class would work with a simple nonce check:
Obviously, calling a class just to run one function doesn’t make a ton of sense. This class is a starting point for you to build in your own authentication system. For example, I recently did a project where I used my internal API for a complex multi-step form. I used the auth class to ensure that the person submitting the form was the “owner” of the current form entry and was using it in the right order.
Responding to requests
Earlier I showed the full example code for the router class. I included (but didn’t discuss) the respond() method. Depending on the range of responses you intend to employ, it may be enough. You might need to build a more complicated class for ensuring your responses are correct.
The respond() method I show simply ensures that it has the right status code, sends a status code header and the response back to the browser. I’m only accounting for the possibility that responses could be true or false or an array that should be returned as JSON.
What about caching?
Part of the beauty of this system it you essentially create a bottleneck which each request passes through. This makes it very easy to introduce caching. Here is a fairly simple modification of the the do_api() method, which uses the WordPress transient API to cache results.
As you can see, the parameters used for the action class, and there values, are used to create a key for the transients. Also, it does not cache on POST requests, since that HTTP method should be used for modifying data. This could present an issue if you have actions without anything in the GET value. You may need to conditionally skip caching based on the current action.
Also, you may wish to use object caching instead. Of course, this only makes sense if you have a persistent object caching system in place. You should make your decision on which caching system you use or don’t use based on performance benchmarks. Caching involves writing additional data to the database or working memory, and you need to make sure that it is worth it.
Now make it your own
That’s the basic outline of how you would configure an internal API for handling high-performance AJAX request processing. As I said at the beginning of this article, this isn’t something you can just paste in and use. It will take some work to get it to match your specific needs and to ensure that it is performant on your server.
The good thing about this type of system is that it makes work easier, as you really only have to do it once. Also, if anything changes, you only need to change it in one place, instead of in all of the places that you hooked into WordPress’ admin AJAX. This, alone, makes it worth it, as less redundant code always equals better code, easier troubleshooting, and fewer mistakes.
6 Comments