An in-depth guide on using Ajax with WordPress

Introduction to the WordPress REST API

The WordPress REST API, in comparison to Admin Ajax, is another monster based on a different planet. Let’s start with what REST is, and go from there.

What is REST?

REST is short for Representational State Transfer. So what the hell does that mean?

REST is basically a way to provide a standard between different servers on the web in order to facilitate communication. The client (the user or script making the request) doesn’t need to know the innards of the REST logic. The client simply knows to pass some data to the REST API, and the API magically presents the data in an easy-to-read format. In the case of WordPress, the response is JSON (JavaScript Object Notation), which is easy to read and parse programmatically.

The goal is that the code for the client and server are independent of another. Meaning, if you designed your REST API correctly, it doesn’t matter which client requests the data: the data returned will always be predictable. Theoretically, the server (in this case the REST API) can update its code as long as the output is the same for all clients. Likewise, the client shouldn’t have to update its code in order to interact with the REST API. If the REST API does change, however, it’s important to version control your APIs and provide backward compatibility so that clients pinging an old version aren’t left stranded.

I like to think of a REST API endpoint as a black box. The client knows what goes in and what is returned, but that’s it. The client has no idea how the endpoint works internally. This allows other clients to access this same data, regardless of where they are located. In a sense, it’s a complete separation of server logic and client logic.

REST API Example
REST API Example

Typically you interact with a REST API by accessing an endpoint and pass along any data the endpoint requires. In the case of WordPress, this could be a user ID, a post ID, or any number of parameters the endpoint requires such as header data, body data, and what type of request you are wanting to make.

The types of request can be summarized as:

  • GET – retrieve data.
  • POST – store data.
  • PUT – update data.
  • DELETE – delete data.

I admit I break a few of these rules when coding out my own REST APIs. For example, creating GET requests using WordPress requires a deep-dive into Regular Expressions for the parameters. I know enough about Regular Expressions (RegEx for short) to make me extremely dangerous, but as one coder told me once: if you attempt to write your own, you’re probably doing it wrong. That shouldn’t dissuade you from trying to write your own GET requests. Just know that they are cumbersome. That being said, I will show a few GET examples later in the chapter.

For a more detailed article about REST and what it is, I suggest checking out this well-written article from Codeacademy.

REST in WordPress

The goal of the REST API in WordPress is to provide a way for others to interact with WordPress sites by sending and receiving data. It shouldn’t matter if the client making the request is a WordPress site. The REST API in WordPress, if setting up the endpoints correctly, should be agnostic about what system is pinging the API.

As a result, you can use WordPress as an application framework and build the client in whatever programming language you desire.

One main use of the WordPress REST API is the new block editor (i.e., Gutenberg). It’s useful to query and store data when creating custom blocks. As mentioned in the official documentation of WordPress, the REST API is language agnostic:

Any programming language which can make HTTP requests and interpret JSON can use the REST API to interact with WordPress, from PHP, Node.js, Go, and Java, to Swift, Kotlin, and beyond.

So what does this all mean in English? It simply states that you can use any programming language to send data to an endpoint and expect a JSON response in which you can parse on the client-side. In the context of Ajax, you’re sending a payload to the REST API, and then parsing this data in JavaScript and perhaps displaying an output to the user.

Ajax REST API Example
Ajax REST API Example

Benefits and Drawbacks of the WordPress REST API

In the WordPress REST API, routes are a list of available endpoints in the REST API. For example, if you have pretty permalinks enabled (and you should), you can navigate to your WordPress site and see the available roots. Just go to https://yourdomain.com/wp-json/ to see all of the available routes.

An endpoint is a final destination for the route. For example, you can go to https://yourdomain.com/wp-json/wp/v2/users to see a list of the users on the site. This brings up an interesting point about security and what data you want to be exposed to third-parties. As a result of security concerns, some turn off the REST API for WordPress or do a hybrid of disabling certain endpoints while enabling others. For example, there is a WordPress plugin that disables the REST API while enabling the whitelisting of custom endpoints. This is one huge drawback of the REST API. As previously mentioned, we had a popular plugin that migrated to using the REST API, but with the high number of active installs, we received enough support requests to switch back to using Admin Ajax.

The positive benefits over Admin Ajax is that you can control every aspect of the REST API’s internals, including permissions and data manipulation. There’s no need to separate permissions such as needed using Admin Ajax. A major benefit is that others can use your REST API endpoint as opposed to Admin Ajax, which is tied to a specific action.

Another arguable drawback of the REST API in WordPress is that it only returns JSON. This makes the REST API a bit less flexible than Admin Ajax. Remember, with Admin Ajax, you can use it to return any form of output, including HTML, JSON, text, XML, and even use it to load custom templates. However, I wouldn’t let the drawbacks of the REST API dissuade you from using it. It’s a standard in WordPress, and you should definitely take advantage of it where possible.

Creating Your First REST API Endpoint

Let’s start out with what action you need to create your first endpoint. The action we’ll be using is called rest_api_init. We’ll start by declaring a procedural namespace to avoid naming collisions in our functions.

<?php
namespace WPAjax;

add_action( 'rest_api_init', 'WPAjax\rest_init' );

/**
 * Initialize the rest routes.
 */
function rest_init() {
	// Rest endpoint code here.
}

Let’s create a GET endpoint that will return users by role.

**
 * Initialize the rest routes.
 */
function rest_init() {
	register_rest_route(
		'wpajax/v1',
		'/users/(?P<role>[a-zA-Z]+)',
		array(
			'methods'  => 'GET',
			'callback' => 'WPAjax\rest_get_roles',
			'args'     => array(
				'role' => array(
					'validate_callback' => 'sanitize_key',
				),
			),
		)
	);
}

We’re using a function called register_rest_route to enable our route. The first argument is the WordPress REST namespace. It’s important to plan this one ahead based on your plugin or theme needs. In this example, I’m using the wpajax namespace and versioning the API at v1. If you were to come up with a version 2 of the API, you’d keep the original endpoint and just re-version the API at v2.

Next, we set up our endpoint. In this case, it’s /users/. Since we’re doing a GET request, we need some parameters passed, which in this case is the user role to return. We’re expecting roles such as administrator, editor, subscriber, or any custom user roles. We do a simple RegEx that allows upper and lower case alphanumeric characters. That should be enough for now.

Next, we define a callback function, which will take care of getting our output. Finally, we define a validation callback, which is tied to the role argument we expect to get passed.

Here’s our callback function:

/**
 * Retrieve users by role.
 *
 * @param WP_REST_Request $request Request array.
 */
function rest_get_roles( $request ) {
	$role    = $request['role'];
	$users   = new \WP_User_Query(
		array(
			'role' => $role,
		)
	);
	$results = $users->get_results();
	if ( $results ) {
		return $results;
	}
	return array(
		'message' => 'No Users Found.',
	);
}

As you can imagine, having an open GET method to show users with roles will impact the security and the privacy of your users. So we’ll use a permissions check in the original endpoint registration. As a result, you may need to pass a nonce to the REST API. First, let’s add the permissions check.

/**
 * Initialize the rest routes.
 */
function rest_init() {
	register_rest_route(
		'wpajax/v1',
		'/users/(?P<role>[a-zA-Z]+)',
		array(
			'methods'             => 'GET',
			'callback'            => 'WPAjax\rest_get_roles',
			'args'                => array(
				'role' => array(
					'validate_callback' => 'sanitize_key',
				),
			),
			'permission_callback' => function () {
				return current_user_can( 'manage_options' );
			},
		)
	);
}

And the REST request would look like:

function get_users() {
	$request  = new \WP_REST_Request( 'GET', '/wpajax/v1/users/administrator' );
	$response = rest_do_request( $request );
	var_dump( $response->data );
}
get_users();

Here’s the full code for reference:

<?php
namespace WPAjax;

add_action( 'rest_api_init', 'WPAjax\rest_init' );

/**
 * Initialize the rest routes.
 */
function rest_init() {
	register_rest_route(
		'wpajax/v1',
		'/users/(?P<role>[a-zA-Z]+)',
		array(
			'methods'             => 'GET',
			'callback'            => 'WPAjax\rest_get_roles',
			'args'                => array(
				'role' => array(
					'validate_callback' => 'sanitize_key',
				),
			),
			'permission_callback' => function () {
				return current_user_can( 'manage_options' );
			},
		)
	);
}

/**
 * Retrieve users by role.
 *
 * @param WP_REST_Request $request Request array.
 */
function rest_get_roles( $request ) {
	$role    = $request['role'];
	$users   = new \WP_User_Query(
		array(
			'role' => $role,
		)
	);
	$results = $users->get_results();
	if ( $results ) {
		return $results;
	}
	return array(
		'message' => 'No Users Found.',
	);
}

function get_users() {
	$request  = new \WP_REST_Request( 'GET', '/wpajax/v1/users/administrator' );
	$response = rest_do_request( $request );
	var_dump( $response->data );
}
get_users();

Here’s the same endpoint coded as a POST endpoint.

/**
 * Initialize the rest routes.
 */
function rest_init() {
	register_rest_route(
		'wpajax/v1',
		'/users',
		array(
			'methods'             => 'POST',
			'callback'            => 'WPAjax\rest_get_roles',
			'args'                => array(
				'role' => array(
					'validate_callback' => 'sanitize_key',
				),
			),
			'permission_callback' => function () {
				return current_user_can( 'manage_options' );
			},
		)
	);
}

And the REST call:

function get_users() {
	$request  = new \WP_REST_Request( 'POST', '/wpajax/v1/users' );
	$request->set_param( 'role', 'administrator' );
	$response = rest_do_request( $request );
	var_dump( $response->data );
}
get_users();

Now that’s a trial-by-fire for REST in WordPress. Let’s move onto an example. Let’s improve upon our Voting Tally plugin by adding in some REST endpoints and returning a little more useful data to the user.

Voting Tally – REST Edition

We’re going to migrate the Voting Tally plugin to use the REST API instead of Admin Ajax. You can follow along at wpajax.pro/rest.

First, let’s set some goals. We’ll want to:

  1. Display visually whether a user has voted or not.
  2. Make it impossible for a user to vote for the same post twice.
  3. Display how many likes a post has received.
  4. Retrieve a list of popular (most liked) posts.
  5. Retrieve a list of recently voted items by the user.

For goals 1-3, we want a user interface like this:

Voting Tally User Interface
Voting Tally User Interface

It shows visually if a user has voted for an item. It also shows how many likes a post has. And if the user would click the thumbs up or thumbs down button, there should be an error if the user has already voted.

User Warning When Trying to Vote Twice
User Warning When Trying to Vote Twice

Now, where do we store this data that a user has voted? We could do post meta. We could do user meta. We could also do the options table. However, storing in any of the above would be a pain to query fast and reliably, especially if we want to display a list of the latest posts a user has voted for. After pain and consideration, it seems we need a second database table to store this data.

Creating the Two Tables

If you remember from the last chapter, we already have one custom table created to store the post votes. This is getting cumbersome already! First, we’ll want to rename the table creation class to something more specific, which in this case will be class-create-voting-table.php. We’ll also create a second table-creation class called class-create-user-table.php.

Voting Tally New Table Files
Voting Tally New Table Files

The code for class-create-voting-table.php will remain mostly unchanged except renaming the class name and a few other things here and there such as docblocks.

<?php
/**
 * Creates and drops the table.
 *
 * @package votingtally
 */

namespace VotingTally\Includes;

/**
 * Class Create_Table
 */
class Create_Voting_Table {

	/**
	 * Holds the tablename for Voting Tally
	 *
	 * @var string $tablename
	 */
	private static $tablename = 'votingtally';

	/**
	 * Class Constructor.
	 */
	public function __construct() {
		$this->create_table();
	}

And the rest of the logic remains the same.

    /**
	 * Retrieves the tablename for the plugin.
	 */
	public static function get_tablename() {
		global $wpdb;
		return $wpdb->base_prefix . self::$tablename;
	}
	/**
	 * Create Voting Tally table
	 *
	 * @since 1.0.0
	 * @access private
	 * @return void
	 */
	private function create_table() {
		global $wpdb;
		$tablename = $wpdb->base_prefix . self::$tablename;

		$version = get_site_option( 'votingtallytable_version', '0' );
		if ( version_compare( $version, VOTINGTALLY_TABLE_VERSION ) < 0 ) {
			$charset_collate = '';
			if ( ! empty( $wpdb->charset ) ) {
				$charset_collate = "DEFAULT CHARACTER SET $wpdb->charset";
			}
			if ( ! empty( $wpdb->collate ) ) {
				$charset_collate .= " COLLATE $wpdb->collate";
			}
			$sql = "CREATE TABLE {$tablename} (
				id INT(20) NOT NULL AUTO_INCREMENT,
				site_id INT (20) NOT NULL,
				blog_id INT (20) NOT NULL,
				content_id INT(20) NOT NULL,
				post_type VARCHAR(20) NOT NULL,
				up_votes INT (20) NOT NULL,
				down_votes INT (20) NOT NULL,
				rating FLOAT (20) NOT NULL,
				PRIMARY KEY  (id)
				) {$charset_collate};";
			require_once ABSPATH . 'wp-admin/includes/upgrade.php';
			dbDelta( $sql );

			update_site_option( 'votingtallytable_version', VOTINGTALLY_TABLE_VERSION );
		}
	}
}

For our second table, it’s more of the same.

<?php
/**
 * Creates and drops the table.
 *
 * @package votingtally
 */

namespace VotingTally\Includes;

/**
 * Class Create_Table
 */
class Create_User_Table {

	/**
	 * Holds the tablename for Voting Tally
	 *
	 * @var string $tablename
	 */
	private static $tablename = 'votingtally_users';

	/**
	 * Class Constructor.
	 */
	public function __construct() {
		$this->create_table();
	}

And the rest.

    /**
	 * Retrieves the tablename for the plugin.
	 */
	public static function get_tablename() {
		global $wpdb;
		return $wpdb->base_prefix . self::$tablename;
	}
	/**
	 * Create Voting Tally table
	 *
	 * @since 1.0.0
	 * @access private
	 * @return void
	 */
	private function create_table() {
		global $wpdb;
		$tablename = $wpdb->base_prefix . self::$tablename;

		$version = get_site_option( 'votingtallytable_user_version', '0' );
		if ( version_compare( $version, VOTINGTALLY_USER_TABLE_VERSION ) < 0 ) {
			$charset_collate = '';
			if ( ! empty( $wpdb->charset ) ) {
				$charset_collate = "DEFAULT CHARACTER SET $wpdb->charset";
			}
			if ( ! empty( $wpdb->collate ) ) {
				$charset_collate .= " COLLATE $wpdb->collate";
			}
			$sql = "CREATE TABLE {$tablename} (
				id INT(20) NOT NULL AUTO_INCREMENT,
				user_id INT (20) NOT NULL,
				post_id INT (20) NOT NULL,
				vote INT(2) NOT NULL,
				PRIMARY KEY  (id)
				) {$charset_collate};";
			require_once ABSPATH . 'wp-admin/includes/upgrade.php';
			dbDelta( $sql );

			update_site_option( 'votingtallytable_user_version', VOTINGTALLY_USER_TABLE_VERSION );
		}
	}
}

We’re using two options here and two constants to track the table versions. The two options are cumbersome, so let’s just keep that in mind in case we want to add more options to the plugin later. Ideally, we could just use one option and store multiple items in them instead of querying for two options manually.

We’ll have to update our main votingtally.php file to use the new constant.

define( 'VOTINGTALLY_VERSION', '1.0.0' );
define( 'VOTINGTALLY_TABLE_VERSION', '1.0.0' );
define( 'VOTINGTALLY_USER_TABLE_VERSION', '1.0.0' );
define( 'VOTINGTALLY_PLUGIN_NAME', 'Voting Tally' );
define( 'VOTINGTALLY_DIR', plugin_dir_path( __FILE__ ) );
define( 'VOTINGTALLY_URL', plugins_url( '/', __FILE__ ) );
define( 'VOTINGTALLY_SLUG', plugin_basename( __FILE__ ) );
define( 'VOTINGTALLY_FILE', __FILE__ );

And finally updating our plugins_loaded method to account for the two new files.

    /**
	 * Fired when the plugins for WordPress have finished loading.
	 */
	public function plugins_loaded() {
		// Create the main table.
		new VotingTally\Includes\Create_Voting_Table();

		// Create the user table.
		new VotingTally\Includes\Create_User_Table();

Creating Several Helper Methods

We’ll need several helper methods in order to get the data out that we need. In the includes folder, let’s create a PHP file named class-helper-functions.php.

<?php
/**
 * Helper functions for displaying Tally's.
 *
 * @package votingtally
 */

namespace VotingTally\Includes;

/**
 * Class Helper_Functions
 */
class Helper_Functions {

}

The first method we’ll concentrate on is the ability to retrieve the popular posts, which we’ll reference in one of our REST API calls. The methods will be declared as static so that we don’t have to instantiate the class in order to access the methods; the methods will be referenced statically.

    /**
	 * Retrieve the most popular posts.
	 *
	 * @param string $post_type The post type to retrieve stats for.
	 * @param int    $posts_per_page The number of items to retrieve.
	 * @param string $order ASC or DESC order.
	 *
	 * @return mixed Object on return, false on failure.
	 */
	public static function get_popular_posts( $post_type, $posts_per_page = 10, $order = 'DESC' ) {
		global $wpdb;
		$post_type      = sanitize_text_field( $post_type );
		$posts_per_page = absint( $posts_per_page );
		$orderby        = sanitize_sql_orderby( 'rating ' . $order );

		// Try to retrieve the cache. Cache by namespace (e.g., votingtally_posts_ASC_24).
		$cache_key = sprintf(
			'votingtally_%s_%s_%d',
			$post_type,
			$order,
			$posts_per_page
		);
		$cache     = wp_cache_get( $cache_key );
		if ( $cache ) {
			return $cache;
		}

		$tablename = Create_Voting_Table::get_tablename();
		$query     = "select * from {$tablename} WHERE post_type = %s order by {$orderby} LIMIT {$posts_per_page}";
		$query     = $wpdb->prepare( $query, $post_type );
		$results   = $wpdb->get_results( $query );

		if ( $results ) {
			foreach ( $results as &amp;$result ) {
				$result->permalink = get_permalink( $result->content_id );
				$result->title     = get_the_title( $result->content_id );
			}
			wp_cache_set( $cache_key, $results, '', 600 ); // Cache for 10 minutes.
			return $results;
		}
		return false;
	}

The above code queries the database for posts based on their rating. The query is cached for 600 seconds (10 minutes) so that we aren’t overloading the database for each popular post call.

Next up is a helper method for getting the recent posts a user has voted for. We’ll be using our new table to query this data. Each time a user votes for a post, it is recorded (we’ll get to that later). Let’s retrieve this data.

    /**
	 * Retrieve Up-voted Posts for User.
	 *
	 * @param int $user_id The User ID to retrieve posts for.
	 *
	 * @return mixed Object on return, false on failure.
	 */
	public static function get_recent_votes_for_user( $user_id ) {
		global $wpdb;
		$user_id = absint( $user_id );

		// Try to retrieve the cache. Cache by namespace (e.g., votingtally_posts_ASC_24).
		$cache_key = sprintf(
			'votingtally_user_%d',
			$user_id
		);
		$cache     = wp_cache_get( $cache_key );
		if ( $cache ) {
			return $cache;
		}

		$tablename = Create_User_Table::get_tablename();
		$query     = "select * from {$tablename} WHERE user_id = %d order by id DESC LIMIT 20";
		$query     = $wpdb->prepare( $query, $user_id );
		$results   = $wpdb->get_results( $query );

		if ( $results ) {
			foreach ( $results as &amp;$result ) {
				$result->permalink = get_permalink( $result->post_id );
				$result->title     = get_the_title( $result->post_id );
			}
			wp_cache_set( $cache_key, $results, '', 600 ); // Cache for 10 minutes.
			return $results;
		}
		return false;
	}

Once again, we’re querying the database. However, this time we’re retrieving the most recent posts a user has voted for. The cache is set for 600 seconds.

The next helper method we’ll be creating is determining a vote for a post. This will allow us to know if a user voted positive or negative for a post in our output. Remember, the goal is to show a visual if a user has voted for a post or not.

Voting Tally User Interface
Voting Tally User Interface

And here’s the code for the helper function.

    /**
	 * Get a user vote for a post.
	 *
	 * @param int $post_id The Post ID.
	 * @param int $user_id The User ID.
	 *
	 * @return int 1 if voted up, 0 if voted down, -1 if no result.
	 */
	public static function get_user_vote( $post_id, $user_id ) {
		global $wpdb;
		$post_id = absint( $post_id );
		$user_id = absint( $user_id );

		$cache_key = sprintf(
			'votingtally_post_%d_user_%d',
			$post_id,
			$user_id
		);
		$cache     = wp_cache_get( $cache_key );
		if ( $cache ) {
			return absint( $cache );
		}

		$tablename = Create_User_Table::get_tablename();
		$query     = "select vote from {$tablename} WHERE user_id = %d and post_id = %d";
		$query     = $wpdb->prepare( $query, $user_id, $post_id );
		$result    = $wpdb->get_var( $query );

		if ( null === $result ) {
			return -1;
		}
		wp_cache_set( $cache_key, $result );
		return absint( $result );
	}

We’re querying the database for a single variable, which is vote. If the result isn’t null, we return an absolute integer (we’re expecting a 1 or 0). Otherwise, we’ll return -1. This will allow us to determine how a user voted for a post (-1 being no vote).

The last method we’ll need is to return the number of positive votes a post has received. This will allow us to show to the user how many positive votes have been recorded.

    /**
	 * Get positive post votes.
	 *
	 * @param int $post_id The Post ID.
	 *
	 * @return int amount of positive votes.
	 */
	public static function get_post_positive_votes( $post_id ) {
		global $wpdb;
		$post_id = absint( $post_id );

		// Try to retrieve the cache. Cache by namespace (e.g., votingtally_posts_ASC_24).
		$cache_key = sprintf(
			'votingtally_post_%d_votes',
			$post_id
		);
		$cache     = wp_cache_get( $cache_key );
		if ( $cache ) {
			return absint( $cache );
		}

		$tablename = Create_Voting_Table::get_tablename();
		$query     = "select up_votes from {$tablename} WHERE content_id = %d";
		$query     = $wpdb->prepare( $query, $post_id );
		$result    = $wpdb->get_var( $query );

		if ( null === $result ) {
			return 0;
		}
		wp_cache_set( $cache_key, $result, '', 600 ); // Cache for 10 minutes.
		return absint( $result );
	}

The above code queries the database for how many up_votes have been recorded. If nothing is found, we simply return zero.

Let’s move onto our enqueue class next and go over what’s changed.

Enqueuing Our Assets

In the last chapter, we used basic enqueueing logic so that our script would only run if the post was singular and the user was logged in. We also set up some variables using wp_localize_script. For the most part, this remains unchanged. Let’s also provide a filter so that other plugin and theme authors can easily override our stylesheet and provide their own.

    /**
	 * Output the Voting Tallery scripts/styles.
	 */
	public function enqueue_scripts() {
		if ( ! is_singular() || ! is_user_logged_in() ) {
			return;
		}
		wp_enqueue_script(
			'votingtally',
			VOTINGTALLY_URL . 'js/votingtally.js',
			array( 'jquery' ),
			VOTINGTALLY_VERSION,
			true
		);
		wp_localize_script(
			'votingtally',
			'votingtally',
			array(
				'rest_url' => rest_url( 'votingtally/v1' ),
				'loading'  => VOTINGTALLY_URL . 'images/loading.svg',
			)
		);

For wp_localize_script, we’ve defined the path to the REST endpoint and have removed some of the messages (we’ll use REST to handle the messages).

Finally, let’s enqueue the stylesheet and provide the filter for overriding the styles.

        /**
		 * Filter so others can easily override styles of the plugin.
		 *
		 * @since 1.0.0
		 *
		 * @param bool true to load the styles.
		 */
		$enqueue_styles = apply_filters( 'voting_tally_enqueue_styles', true );
		if ( $enqueue_styles ) {
			wp_enqueue_style(
				'votingtally',
				VOTINGTALLY_URL . 'css/votingtally.css',
				array(),
				VOTINGTALLY_VERSION,
				'all'
			);
		}
	}

In the above example, we’ve created a filter called voting_tally_enqueue_styles. Other authors can tap into this filter and easily remove our stylesheet and provide their own.

Let’s move onto the main interface output, which will output or thumbs up and thumbs down functionality.

Main Interface Output

As mentioned in our initial requirements, we want to display whether a user has voted. This will require some stylesheet changes, and also adding a class to the interface on which item a user has voted. We also want to display the number of likes a post has received.

    /**
	 * Output the Voting Tallery button interface.
	 *
	 * @param string $content The Post content.
	 *
	 * @return string Modified content.
	 */
	public function maybe_output_interface( $content ) {
		if ( ! is_singular() || ! is_user_logged_in() ) {
			return $content;
		}
		global $current_user;
		$vote = Helper_Functions::get_user_vote( get_queried_object_id(), $current_user->ID );
		ob_start();
		?>	
		<div class="voting-tally">
			<h5><?php esc_html_e( 'Rank This Post', 'votingtally' ); ?></h5>
			<button class="vote-upwards tally-button <?php echo 1 === $vote ? esc_attr( 'active' ) : ''; ?>" aria-label="<?php esc_attr_e( 'Vote this item up', 'votingtallery' ); ?>" data-nonce="<?php echo esc_html( wp_create_nonce( 'wp_rest' ) ); ?>" data-id="<?php echo absint( get_the_ID() ); ?>" data-action="1">
				<img src="<?php echo esc_url( VOTINGTALLY_URL . 'images/thumbs-up.png' ); ?>" alt="Thumbs Up Button" />
			</button>
			<button class="vote-downwards tally-button <?php echo 0 === $vote ? esc_attr( 'active' ) : ''; ?>" aria-label="<?php esc_attr_e( 'Vote this item down', 'votingtallery' ); ?>" data-nonce="<?php echo esc_html( wp_create_nonce( 'wp_rest' ) ); ?>" data-id="<?php echo absint( get_the_ID() ); ?>" data-action="0">
				<img src="<?php echo esc_url( VOTINGTALLY_URL . 'images/thumbs-down.png' ); ?>" alt="Thumbs Down Button" />
			</button>
			<div class=voting-tally-num-votes">
				<?php
				$num_votes = number_format( Helper_Functions::get_post_positive_votes( get_queried_object_id() ) );
				/* Translators: %s is the number of post likes */
				$message = sprintf( esc_html__( '%s Likes', 'votingtally' ), $num_votes );
				echo esc_html( $message );
				?>
			</div>
		</div>
		<?php
		return $content . ob_get_clean();
	}

In the above code, we utilize our helper method called get_user_vote to retrieve the current vote for the item based on if the user has voted before. If the vote is a 1, we provide an active class to that item. Likewise, if the vote is 0 (a vote down), we provide an active class for that. Note the use of esc_attr. We’re escaping a regular hard-coded string. Why in the world would we do that? It’s simply because the string may be dynamic in the future. Who knows? It’s just safer to escape everything in case something changes in the future.

Finally, we display the number of votes by using the helper function get_post_positive_votes.

Now our interface is set, aside from styling, so let’s move onto that next.

Styling the Interface

The styling has changed just a tiny-bit since the last chapter. Let’s concentrate first on styling the container and the thumbs up/down images. I’m not a fan of using !important tags, but again, the Twenty Twenty theme makes it difficult to style certain items semantically.

.voting-tally {
	background: #FFF;
	border: 1px solid #DDD;
	text-align: center;
	display: block;
	width: auto !important;
	margin: 0 auto;
	padding: 20px;
}
.voting-tally img {
	display: inline-block;
}
.voting-tally h5 {
	padding: 0 !important;
	margin: 0 !important;
}
.voting-tally button.tally-button img {
	max-width: 50px;
	margin: 0;
	padding: 0;
}

Let’s work on styling the buttons next.

.voting-tally button.tally-button {
	display: inline-block;
	border: none;
	padding: 1rem 2rem;
	margin: 0;
	text-decoration: none;
	background: #FFF;
	color: #ffffff;
	font-family: sans-serif;
	font-size: 1rem;
	cursor: pointer;
	text-align: center;
	transition: background 250ms ease-in-out, 
	transform 150ms ease;
	-webkit-appearance: none;
	-moz-appearance: none;
}

And finally, we account for the active states for the buttons.

.voting-tally button.tally-button:hover,
.voting-tally button.tally-button:focus,
.voting-tally button.tally-button.active {
	background: rgb(250, 250, 250);
	border: 1px solid #EEE;
}

.voting-tally button.tally-button:focus {
	outline: 1px solid #fff;
	outline-offset: -4px;
}

.voting-tally button.tally-button:active,
.voting-tally button.tally-button.active {
	transform: scale(0.99);
}

We’re now ready for our REST API calls, so let’s move onto that one next.

Initializing the REST Calls and Callback Methods

We’ll be creating a PHP file in our includes folder called class-rest.php.

<?php
/**
 * Captures the Ajax Calls.
 *
 * @package votingtally
 */

namespace VotingTally\Includes;

/**
 * Class Rest
 */
class Rest {
	/**
	 * Class Constructor.
	 */
	public function __construct() {

	}
}

We’ll be making use of the rest_api_init action in order to register our routes and endpoints.

<?php
/**
 * Captures the Ajax Calls.
 *
 * @package votingtally
 */

namespace VotingTally\Includes;

/**
 * Class Rest
 */
class Rest {
	/**
	 * Class Constructor.
	 */
	public function __construct() {
		// Rest API.
		add_action( 'rest_api_init', array( $this, 'register_rest_endpoints' ) );
	}

	/**
	 * Register the REST endpoints this plugin needs.
	 */
	public function register_rest_endpoints() {
	}

In the above example, we use rest_api_init with a callback of register_rest_endpoints. Let’s take a dive into that method to initialize some REST calls.

    /**
	 * Register the REST endpoints this plugin needs.
	 */
	public function register_rest_endpoints() {
		register_rest_route(
			'votingtally/v1',
			'/record_vote/',
			array(
				'methods'  => 'POST',
				'callback' => array( $this, 'rest_record_vote' ),
			)
		);

We register a route with a namespace of votingtally and an API version of v1. The endpoint is named /record-vote/ and a callback method for the endpoint (when matched) is the method rest_record_vote. Since we’re storing (creating) data, we’re using a POST call.

Let’s register another route, this time being for getting the popular posts. Since we’re retrieving data, this is a GET call.

    /**
	 * Register the REST endpoints this plugin needs.
	 */
	public function register_rest_endpoints() {
		register_rest_route(
			'votingtally/v1',
			'/record_vote/',
			array(
				'methods'  => 'POST',
				'callback' => array( $this, 'rest_record_vote' ),
			)
		);
		register_rest_route(
			'votingtally/v1',
			'/get_posts/(?P<post_type>[a-zA-Z]+)/(?P<posts_per_page>\d+)/(?P<order>[A-Z]+)',
			array(
				'methods'  => 'GET',
				'callback' => array( $this, 'rest_get_posts' ),
			)
		);
	}

The GET endpoint expects a post_type argument, a posts_per_page argument, and finally an order (ASC or DESC). Let’s move into the method rest_record_vote. You’ll find the logic similar to the callback method we used in Admin Ajax. The main difference is the return type. We’ll return a WordPress error packet if the callback method has an error. Otherwise, we’ll return a normal REST response.

Let’s break this callback into pieces so I can explain what’s going on.

    /**
	 * Capture the Recorded Vote.
	 *
	 * @param array $request Request array passed via Ajax.
	 */
	public function rest_record_vote( $request ) {
		global $current_user;
		if ( ! is_user_logged_in() ) {
			return new \WP_Error(
				'user_not_logged_in',
				__( 'User not logged in.', 'votingtally' ),
				array( 'status' => 404 )
			);
		}

		// Retrieve the vote.
		$vote = absint( $request['vote'] );

		// Retrieve the post ID.
		$post_id   = absint( $request['post_id'] );
		$post_type = get_post_type( $post_id );

		// Get the current site information.
		global $current_blog, $wpdb;
		$site_id = 1;
		$blog_id = 1;
		if ( is_multisite() ) {
			$site_id = absint( $current_blog->site_id );
			$blog_id = absint( $current_blog->blog_id );
		}

		// Check if the user has already voted.
		$user_table  = Create_User_Table::get_tablename();
		$user_result = $wpdb->get_row(
			$wpdb->prepare(
				"select * from {$user_table} where user_id = %d AND post_id = %d",
				$current_user->ID,
				$post_id
			)
		);
		if ( $user_result ) {
			return new \WP_Error(
				'user_voted',
				__( 'You have already voted!', 'votingtally' ),
				array( 'status' => 404 )
			);
		}

First, we’ll check if the user is logged in. If not, we’ll return a message (via WP_Error) and return a status code of 404 (meaning the REST call failed). We then initialize some passed variables, check if the user has voted before (and return an error if so), and begin storing the data otherwise.

        // Get the post rating.
		$post_rating = $this->get_post_stats( $post_id );
		$tablename   = Create_Voting_Table::get_tablename();
		if ( ! $post_rating ) {
			// Insert new row because it doesn't exist.
			$wpdb->insert(
				$tablename,
				array(
					'site_id'    => absint( $site_id ),
					'blog_id'    => absint( $blog_id ),
					'content_id' => absint( $post_id ),
					'post_type'  => sanitize_text_field( $post_type ),
				),
				array( '%d', '%d', '%d', '%s' )
			);
			$post_rating              = new \stdClass();
			$post_rating->up_votes    = 0;
			$post_rating->down_votes  = 0;
			$post_rating->total_votes = 0;
		}
		$post_rating->total_votes = $post_rating->up_votes + $post_rating->down_votes;
		// Make sure results aren't negative.
		$post_rating->up_votes    = $post_rating->up_votes < 0 ? 0 : $post_rating->up_votes;
		$post_rating->down_votes  = $post_rating->down_votes < 0 ? 0 : $post_rating->down_votes;
		$post_rating->total_votes = $post_rating->total_votes < 0 ? 0 : $post_rating->total_votes;

		// Let's get the total of up and down votes.
		if ( 1 === $vote ) {
			$post_rating->up_votes += 1;
			$vote                   = true;
		} else {
			$post_rating->down_votes += 1;
			$vote                     = false;
		}

		// Update the post.
		$wpdb->update(
			$tablename,
			array(
				'up_votes'   => $post_rating->up_votes,
				'down_votes' => $post_rating->down_votes,
			),
			array(
				'site_id'    => $site_id,
				'blog_id'    => $blog_id,
				'content_id' => $post_id,
			),
			array( '%d', '%d', '%d' )
		);

The above calls an internal method called get_post_stats, which retrieves the stats for a particular post. We then update the voting table with the number of upvotes and downvotes. Let’s go into the get_post_stats method before finishing out the REST callback.

    /**
	 * Get rating stats for a post ID.
	 *
	 * @param int $post_id The Post ID to retrieve stats for.
	 *
	 * @return mixed false on failure, object on success.
	 */
	private function get_post_stats( $post_id = 0 ) {
		global $current_blog, $wpdb;
		$site_id = 1;
		$blog_id = 1;
		if ( is_multisite() ) {
			$site_id = $current_blog->site_id;
			$blog_id = $current_blog->blog_id;
		}
		$tablename = Create_Voting_Table::get_tablename();
		$sql       = "select * from {$tablename} where content_id = %d and site_id = %d and blog_id = %d";
		$results   = $wpdb->get_row( $wpdb->prepare( $sql, $post_id, $site_id, $blog_id ) ); // phpcs:ignore
		if ( $results ) {
			return $results;
		}
		return false;
	}

The above simply returns a row object if the post has been voted on before. Otherwise, false is returned if no results are found. Let’s finalize the method for rest_record_vote.

        // Record the vote for the user.
		$user_table = Create_User_Table::get_tablename();
		$wpdb->insert(
			$user_table,
			array(
				'user_id' => $current_user->ID,
				'post_id' => $post_id,
				'vote'    => $vote,
			),
			array( '%d', '%d', '%d' )
		);

The above will create a post-voting entry in the user’s table so we can track whether a user has voted before and also be able to retrieve the posts a user has voted for.

Finally, we use algorithm magic to record the up or down vote.

// Spiffy, now let's calculate the total ratings for everything and update.
$total_items = $wpdb->get_var( $wpdb->prepare( "select count( id ) from {$tablename} where site_id = %d and blog_id = %d and post_type = '%s'", $site_id, $blog_id, $post_type ) );

$results = $wpdb->get_row( $wpdb->prepare( "select SUM(up_votes + down_votes) as total_votes, SUM( ( up_votes * 5 + down_votes * 1 ) / {$total_items} ) as ratings_sum  from {$tablename} where site_id = %d and blog_id = %d and post_type = %s and content_id = %d", $site_id, $blog_id, $post_type, $post_id ) );

$args = array(
	'total_items'    => $total_items,
	'total_votes'    => $results->total_votes,
	'average_rating' => $results->ratings_sum / $total_items,
	'average_votes'  => $results->total_votes / $total_items,
);

$sql = $wpdb->prepare( "UPDATE {$tablename} set rating = ( ( {$args['average_votes']} * {$args['average_rating']} ) + ( ( up_votes + down_votes ) * ( up_votes * 5 + down_votes * 1 ) / ( up_votes + down_votes ) ) ) / {$args['average_votes']} + up_votes + down_votes where site_id = %d and blog_id = %d and post_type = %s and content_id = %d", $site_id, $blog_id, $post_type, $post_id );
$wpdb->query( $sql );
return new \WP_REST_Response(
	array(
		'message' => __( 'Your vote has been recorded.', 'votingtally' ),
	),
	200
);

Note the use of WP_Rest_Response. We can use this built-in class in WordPress to return a message and a 200 status code if everything is fine.

Finally, I’ll show you the rest_get_posts callback for retrieving the popular posts.

/**
 * Get the popular posts.
 *
 * @param array $request Request array passed via Ajax.
 */
public function rest_get_posts( $request ) {
	$posts = Helper_Functions::get_popular_posts(
		$request['post_type'],
		$request['posts_per_page'],
		$request['order']
	);
	if ( $posts ) {
		return new \WP_REST_Response(
			$posts,
			200
		);
	}
	return new \WP_Error(
		'no_posts',
		__( 'There are not any popular posts to display.', 'votingtally' ),
		array( 'status' => 404 )
	);
}

We use our helper method get_popular_posts to retrieve the popular posts. If found, we return a WP_REST_Response with the posts. Otherwise, we return an error message.

Let’s move onto the JavaScript needed to record a vote using the REST API.

Interacting With the REST API Using JavaScript

Interacting with the REST API via JavaScript is very similar to the technique used in Admin Ajax. However, we need to pass specific data and headers, so we’re going to avoid using shorthand for the jQuery Ajax call.

jQuery(function($) {
	$( '.tally-button' ).on( 'click', function( e ) {
		e.preventDefault();
		var html = '<img src="' + votingtally.loading + '" alt="Loading Animation" />';
		$( '.voting-tally' ).html( html );
		$.ajax( {
			url: votingtally.rest_url + '/record_vote/',
			type: 'post',
			data: {
				post_id: $( this ).data( 'id' ),
				vote: $( this ).data('action'),
			},
			headers: {
				'X-WP-Nonce': $( this ).data('nonce'),
			},
			success: function( data ) {

			},
			error: function( data ) {

			}
		} )
		.done(function( response ) {
			$( '.voting-tally' ).html( '<h5>' + response.message + '</h5>' );
			
		})
		.fail(function( response ) {
			$( '.voting-tally' ).html( '<h5>' + response.responseJSON.message + '</h5>' );
		})
		.always(function( data ) {
			
		});

			
	} );
});

Let’s circle back to the interface output before explaining out the JavaScript.

<button class="vote-upwards tally-button <?php echo 1 === $vote ? esc_attr( 'active' ) : ''; ?>" aria-label="<?php esc_attr_e( 'Vote this item up', 'votingtallery' ); ?>" data-nonce="<?php echo esc_html( wp_create_nonce( 'wp_rest' ) ); ?>" data-id="<?php echo absint( get_the_ID() ); ?>" data-action="1">
				<img src="<?php echo esc_url( VOTINGTALLY_URL . 'images/thumbs-up.png' ); ?>" alt="Thumbs Up Button" />
			</button>
			<button class="vote-downwards tally-button <?php echo 0 === $vote ? esc_attr( 'active' ) : ''; ?>" aria-label="<?php esc_attr_e( 'Vote this item down', 'votingtallery' ); ?>" data-nonce="<?php echo esc_html( wp_create_nonce( 'wp_rest' ) ); ?>" data-id="<?php echo absint( get_the_ID() ); ?>" data-action="0">
				<img src="<?php echo esc_url( VOTINGTALLY_URL . 'images/thumbs-down.png' ); ?>" alt="Thumbs Down Button" />
			</button>

In the above interface code, we set several data- variables that we can capture using JavaScript. Of note is the nonce variable, which has an action called wp_rest. This nonce is valuable as we can use capability and permissions checks in our REST call if passed.

Back to the JavaScript… We point to the REST API using one of our localized variables. We then set the data object with our data attributes. Finally, we set a header of X-WP-Nonce, which is necessary to pass the nonce value for permission and capability checks. Finally, we have a done and fail callback. If the REST API 404’s, the fail callback is run. Otherwise, the done callback is run on success.

REST API Recap

So far we have:

  • Created two tables to store and retrieve post and user data.
  • Defined constants in the main plugin file for table versioning.
  • Created several helper methods to retrieve the post and user data.
  • Cleaned up our enqueue class and allowed other authors to override our styles using a filter.
  • Tweaked the main output interface to account for previous votes and an overall tally of “likes.”
  • Styled the interface.
  • Created the REST API endpoints and routes.
  • Used JavaScript to record a vote or display an error if a user has already voted.

What haven’t we done yet? We haven’t provided a way to display popular posts or the user’s recent votes. Let’s finish this out by creating a few shortcodes to display this output.

For the output, we’ll be creating a new PHP class file named class-shortcode.php. Let’s initialize the class and shortcodes.

<?php
/**
 * Shortcode for outputting posts.
 *
 * @package votingtally
 */

namespace VotingTally\Includes;

/**
 * Class Output
 */
class Shortcode {
	/**
	 * Class Constructor.
	 */
	public function __construct() {
		add_shortcode( 'votingtally', array( $this, 'shortcode_votingtally' ) );
		add_shortcode( 'votingtally_user', array( $this, 'shortcode_votingtally_user' ) );
	}

So far we’ve created two shortcodes: votingtally and votingtally_user. Let’s move to the callback methods for the two shortcodes (shortcode_votingtally and shortcode_votingtally_user respectively).

/**
 * Output the Voting Talley popular posts items.
 *
 * @param array $atts The shortcode attributes.
 *
 * @return string Shortcode content.
 */
public function shortcode_votingtally( $atts ) {
	if ( is_admin() ) {
		return '';
	}
	$atts        = shortcode_atts(
		array(
			'post_type'      => 'post',
			'posts_per_page' => 10,
			'order'          => 'DESC',
		),
		$atts,
		'votingtally'
	);
	$body        = array(
		'post_type'      => $atts['post_type'],
		'posts_per_page' => $atts['posts_per_page'],
		'order'          => $atts['order'],
	);
	$endpoint    = sprintf(
		'/votingtally/v1/get_posts/%s/%d/%s',
		$atts['post_type'],
		$atts['posts_per_page'],
		$atts['order']
	);
	$maybe_posts = wp_safe_remote_get(
		esc_url( rest_url( $endpoint ) )
	);
	if ( is_wp_error( $maybe_posts ) ) {
		return '';
	}
	$remote_body = json_decode( wp_remote_retrieve_body( $maybe_posts ) );
	if ( $remote_body ) {
		ob_start();
		printf(
			'<h2>%s</h2>',
			esc_html__( 'Popular Items', 'votingtally' )
		);
		echo '<ol>';
		foreach ( $remote_body as $post_data ) {
			printf(
				'<li><a href="%s">%s</a></li>',
				esc_url( $post_data->permalink ),
				esc_html( $post_data->title )
			);
		}
		echo '</ol>';
		return ob_get_clean();
	}
	return '';
}

This one is fairly involved. It sets default attributes and then sets up the GET endpoint in order to ping the REST API. Granted, this is overkill. Ideally, we’d skip the REST API here and just return the output directly without having to do an HTTP API call. However, I wanted to demonstrate how to ping the REST API using wp_safe_remote_get. If it’s not an error, we decode the JSON and begin outputting the popular items. Of note is the use of ob_start and ob_get_clean. Since shortcodes should always return content, we make use of output buffering to capture string output. It’s not needed here, but again, this is for demonstration purposes in case you need to output complex HTML in a shortcode.

Let’s move onto the next shortcode.

/**
 * Output the recent posts a user has voted for.
 *
 * @param array $atts The shortcode attributes.
 *
 * @return string Shortcode content.
 */
public function shortcode_votingtally_user( $atts ) {
	if ( is_admin() || ! is_user_logged_in() ) {
		return '';
	}
	global $current_user;
	$user_id = $current_user->ID;
	$posts   = Helper_Functions::get_recent_votes_for_user( $user_id );
	if ( $posts ) {
		ob_start();
		printf(
			'<h2>%s</h2>',
			esc_html__( 'Your Recent Votes', 'votingtally' )
		);
		echo '<ol>';
		foreach ( $posts as $post_data ) {
			printf(
				'<li><a href="%s">%s</a> (%d)</li>',
				esc_url( $post_data->permalink ),
				esc_html( $post_data->title ),
				absint( $post_data->vote )
			);
		}
		echo '</ol>';
		return ob_get_clean();
	}
	return '';
}

This one retrieves a list of posts that a user has previously voted for. Instead of a REST call, we use the helper function get_recent_votes_for_user. We then format and return the output.

Finishing Up

Here is our final output from votingtally.php's plugins_loaded callback.

/**
 * Fired when the plugins for WordPress have finished loading.
 */
public function plugins_loaded() {
	// Create the main table.
	new VotingTally\Includes\Create_Voting_Table();

	// Create the user table.
	new VotingTally\Includes\Create_User_Table();

	// Output the Voting Talley interface.
	new VotingTally\Includes\Output();

	// Enqueue the necessary scripts/styles.
	new VotingTally\Includes\Enqueue();

	// Register Rest Calls.
	new VotingTally\Includes\Rest();

	// Register Shortcode.
	new VotingTally\Includes\Shortcode();
}

Now we should have a fully functional user interface for voting up or down on posts. We also have two shortcodes we can use to display the popular posts and recently-voted-on items for the user.

You can view the full code at wpajax.pro/rest and even install and play with it to get the output you desire.

Conclusion

In this chapter, we learned:

  • What REST is.
  • REST in the context of WordPress.
  • How to create REST routes and endpoints.
  • How to modify an existing plugin to use REST instead of Admin Ajax.

In the next chapter, we’ll build upon the REST plugin we created in this chapter and dive into creating a few Gutenberg blocks to replace our shortcodes.

Leave a Comment

Your email address will not be published. Required fields are marked *

Scroll to Top