As a WordPress developer, learning object-oriented programming seems daunting at first. It’s one thing to learn all the concepts, but theory will only get you so far.
You want to see how you can do it in practice. How do you start from nothing and end up with a working class. That’s why I’m going to share how I built a client for the WordPress REST API. You’ll get to see how I use object-oriented design in practice.
Some background
I’ve been working on a project with the WordPress REST API over the last few weeks. The goal of the project was to do a complex cross-site user importer. I decided to use that API because I’m more confortable with it than with the XML-RPC API.
The importer itself needed a generic php client to connect with the API. The WP-API project does offer a PHP client. That said, it wasn’t designed to leverage internal WordPress functionalities. The project team build the client for the larger PHP community.
This prompted me to design a WordPress API client. I had two goals in mind for it. It had to:
1) Leverage WordPress APIs
2) Work without an external library
Adding some limitations
Before we get into it, let’s talk about the two limitations that I’m imposing on the article. The point of those limitations is to limit the amount of code in the class. It also makes it easier for you to understand the design decisions.
You’ll see that there’s plenty to cover already with those limitations.
One API method
The client will only have one API method. That’s how to retrieve users. That said, that one method will give you the foundation you need for creating the other ones yourself.
Basic authentication
There’s several authentication methods available for the API. We’re going to use basic authentication for the client. It isn’t recommended for a production environment, but it’s fine for the context of this example.
Authentication is only a small part of the client functionality. That means you can reuse a lot of the code here with the other authentication methods.
There’s a few ways you could handle the different authentication methods. You could create classes for each with a common interface. You could also create an abstract class and extend it for each authentication method. Both have their advantages and disadvantages.
How will others interact with the client?
In most cases, this is the first thing you should ask yourself. How is another programmer going to interact with it. It forces you to think about the job your class needs to do. You also need to figure out what it needs to have to do its job.
This ties into the single responsibility principle. You want to focus on the essential and not have your class do everything. Let’s go over the decisions I took.
Constants
I wanted the path to the endpoint for retrieving users to be a constant. We want to use a constant when we know a value won’t change ever. The endpoint fits that idea. It shouldn’t change or the client will break.
class WP_API_Client { /** * Base path for all API user resources. * * @var string */ const ENDPOINT_USERS = '/wp-json/wp/v2/users'; // ... }
Constructor
class WP_API_Client { /** * Base URL for the WordPress site that the client is connecting to. * * @var string */ private $base_url; /** * WordPress HTTP transport used for communication. * * @var WP_Http */ private $http; /** * The authorization token used by the client. * * @var string */ private $token; /** * Constructor. * * @param WP_Http $http * @param string $base_url * @param string $token */ public function __construct(WP_Http $http, $base_url, $token) { $this->http = $http; $this->base_url = $base_url; $this->token = $token; } // ... }
I designed the standard constructor to take three parameters. Two of those are straight forward. You need the base URL to the WordPress site you are connecting to. You also need the authorization token used by the basic authentication.
I’d like to point out we pass the encoded token. You could pass the username and the password instead. The constructor would need to encoded them.
class WP_API_Client { /** * Constructor. * * @param WP_Http $http * @param string $base_url * @param string $username * @param string $password */ public function __construct(WP_Http $http, $base_url, $username, $password) { $this->http = $http; $this->base_url = $base_url; $this->token = base64_encode($username . ':' . $password); } // ... }
The final parameter is an instance of WP_Http
. It’s a WordPress class that most developers don’t know exists. It powers the HTTP API. It hides the complex logic WordPress does behind the scenes to allow you to do an HTTP request.
The reason you want to pass that class to the constructor is to follow SOLID principles. It isn’t the job of our API class to handle all the logic related to performing an HTTP request. You want to let WP_Http
do that job for you and pass it in the constructor.
Create an instance from WordPress globals
Now that you have a constructor, you want to add a layer to decouple the WordPress functionality from your class. You do this by using a static method to construct the object. The job of that static method is to create the client from the WordPress globals.
class WP_API_Client { /** * Creates an API client from WordPress global objects. * * @param string $base_url * @param string $token * * @return WP_API_Client */ public static function create($base_url, $token) { return new self(_wp_http_get_object(), $base_url, $token); } // ... }
A client method that matches the API method
This was an important design consideration. The client method needed to match the API method in the plugin. This meant using the same name and parameters as the API method. By doing this, you make it easy for another developer to use the client by looking up the API documentation.
It’s worth noting that there was a lot of back and forth here. The parameters on the documentation site weren’t working as described. Some were also missing. It’s not unusual for the documentation to lag behind.
Whenever you encounter an issue like that, you want to dig into the code to see what’s happening. You also want to run some tests to determine how the API is working in practice. This is what I did here.
class WP_API_Client { /** * Retrieve a subset of the site's users. * * @param array $filters * @param string $context * @param integer $page * * @return array|WP_Error */ public function get_users(array $filters = array(), $context = 'view', $page = 1) { // ... } // ... }
You’ll notice that I left the inside of the method blank. This is intentional. We’ll cover it in the next section.
What’s going on behind the scene
Now, that we’ve defined what is accessible from outside the class. It’s time to work on what’s going on inside the class. The idea is to work your way from the public method and create internal methods to help it do its job.
I’ve tried to break these down into categories in the order I tackled them. You’ll notice that it usually involves diving one level down from the previous category.
Doing a HTTP request
Let’s go back to the get_users
method. What do you need to code to make it work? It’s important to take a step back and think about it before you start coding. So take a second to think about it before reading on.
Building the request URL
We need a way to build the request URL to communicate with the API. We have several components that we need to combine to create the URL. They are:
- The base URL
- The path to the endpoint
- The query string arguments
Let’s take a look at the method to see the design decisions I took.
class WP_API_Client { /** * Builds a full API request URL from the given endpoint URL and query string arguments. * * @param string $endpoint * @param array $query * * @return string */ private function build_url($endpoint, array $query = array()) { $url = $this->base_url.$endpoint; if (!empty($query)) { $url .= '?'.http_build_query($query); } return $url; } // ... }
Let’s talk about the parameters. We don’t pass the base URL because the idea is to build an URL to that points to it. So we only pass the endpoint and the query string arguments as an array. The query string arguments are optional since you might not always have any.
Building the URL is pretty straightforward. I want to combine the base URL and the endpoint path. If we have query string arguments, we want to add them to the URL. PHP has a built-in function to do this for you.
Sending a GET request
We want to send a GET request with the URL that build_url
built. You’ll need an internal method to send it with the WP_Http
object we passed in the constructor.
class WP_API_Client { /** * Retrieve a subset of the site's users. * * @param array $filters * @param string $context * @param integer $page * * @return array|WP_Error */ public function get_users(array $filters = array(), $context = 'view', $page = 1) { return $this->get($this->build_url(self::ENDPOINT_USERS, array('filter' => $filters, 'context' => $context, 'page' => $page))); } // ... }
So now that we filled out our get_users
method, let’s dig down to HTTP API.
Working with the HTTP API
The HTTP API is great for hiding away the complexity of doing an HTTP request with WordPress. There’s still a lot of work that our client needs to do once we get a response back. You need to convert the response into something the client can use.
class WP_API_Client { /** * Performs a GET request using the WordPress HTTP transport. Returns a WP_Error * on error. * * @param string $url * @param array $args * * @return array|WP_Error */ private function get($url, array $args = array()) { $response = $this->http->get($url, $this->build_args($args)); // ... } // ... }
We get our response by calling the get
method from the WP_Http
class. You want to pass it the URL we built in get_users
. You also want to pass it some arguments. That’s how the API builds its request.
class WP_API_Client { /** * Builds the WordPress HTTP transport arguments. * * @param array * * @return array */ private function build_args(array $args = array()) { return array_merge_recursive($args, array( 'headers' => array( 'Authorization' => 'Basic '.$this->token, ), )); } // ... }
We need to add our own build_args
method to add the basic authentication token from the constructor. This is how we’ll authenticate with the API since we’re using basic authentication. This code would be different depending on your chosen authentication method.
Processing the response
At this point, I took an important design decision. I wanted two possible return values from the client. If we had a valid response from the API, the return value should be the decoded JSON array.
Otherwise, we should be getting an instance of WP_Error
. This is to stay in the spirit of how WordPress handles errors.
Getting information from the response
Before we proceed further, let’s talk about the response we get from the HTTP API. The HTTP API always gives you either an array or an instance of WP_Error
. The fact that the response is an array adds some extra work you need to take care of.
You shouldn’t assume that your array is correctly formatted. You’ll need to add some methods to validate and extract that information. You want to be able to handle if the information isn’t there. If you try to work with the array without validating, your code could generate you warnings or flat out break.
For the client, I needed to extract the response headers and the status code. You’ll see why when we start working with the response.
class WP_API_Client { /** * Extracts the response headers from the given response. * * @param array * * @return array */ private function get_response_headers(array $response) { if (!isset($response['headers']) || !is_array($response['headers'])) { return array(); } return $response['headers']; } /** * Extracts the status code from the given response. * * @param array $response * * @return int|null */ private function get_response_status_code(array $response) { if (!isset($response['response']) || !isset($response['response']['code'])) { return null; } return $response['response']['code']; } // ... }
This shows you the advantage of using an object over an array. You need to repeat this code in any class that uses the HTTP API. If we had a response class, you wouldn’t need to write it anywhere.
Decoding the response
Let’s go back to the get
method. The first thing we want to do is handle a successful response from the API.
class WP_API_Client { /** * Performs a GET request using the WordPress HTTP transport. Returns a WP_Error * on error. * * @param string $url * @param array $args * * @return array|WP_Error */ private function get($url, array $args = array()) { $response = $this->http->get($url, $this->build_args($args)); if (is_array($response) && $this->is_successful($response)) { $response = $this->decode_response($response); } // ... return $response } // ... }
A successful response from the HTTP API will be an array. We check for that first. We also want the response to have a successful status code.
class WP_API_Client { /** * Checks if the given response is considered successful as per the HTTP specification. * This means that the response has a 2xx status code. * * @param array $response * * @return bool */ private function is_successful(array $response) { $status_code = $this->get_response_status_code($response); if (null === $status_code) { return false; } return $status_code >= 200 && $status_code < 300; } // ... }
If we have an array and the response is successful, we want to decode it.
class WP_API_Client { /** * Decodes the JSON object returned in given response. Returns a WP_Error on error. * * @param array $response * * @return array|WP_Error */ private function decode_response(array $response) { $decoded = array(); $headers = $this->get_response_headers($response); if (!isset($headers['content-type']) || false === stripos($headers['content-type'], 'application/json')) { return new WP_Error('invalid_response', 'The content-type of the response needs to be "application/json".'); } if (isset($response['body'])) { $decoded = json_decode($response['body'], true); } if (null === $decoded) { return new WP_Error('invalid_json', 'The JSON response couldn\'t be decoded.'); } return $decoded; } // ... }
Before we do anything, you want to check that the response is actually JSON. You’ll want to check the headers to see if we have application/json
in the content-type
. You want to enforce that the content of a response is JSON. This prevents errors where the response was something else like HTML.
Now that we validated the content type, we want to decode the response. You want to do a quick check to see if there’s a response body first though.
json_decode
will return null if it there was an error decoding the JSON. You want to catch that error and return a WP_Error
instead. If you make it to the end, you’ll have either an empty array or the decoded JSON array.
Converting API errors
The other situation we want to handle is an unsuccessful response from the API. That’s because if there’s an error, the API will return the JSON encoded WP_Error
. I wanted to convert those back into a WP_Error
object.
class WP_API_Client { /** * Performs a GET request using the WordPress HTTP transport. Returns a WP_Error * on error. * * @param string $url * @param array $args * * @return array|WP_Error */ private function get($url, array $args = array()) { $response = $this->http->get($url, $this->build_args($args)); if (is_array($response) && $this->is_successful($response)) { $response = $this->decode_response($response); } elseif (is_array($response) && !$this->is_successful($response)) { $response = $this->convert_response_to_error($response); } return $response; } // ... }
So the new condition is that we want the response to be an array, but is_successful
needs to be false.
class WP_API_Client { /** * Converts the given response to a WP_Error object. * * @param array $response * * @return WP_Error */ private function convert_response_to_error(array $response) { $response = $this->decode_response($response); $error = new WP_Error(); if ($response instanceof WP_Error) { $error = $response; } elseif (is_array($response)) { array_walk($response, array($this, 'add_response_error'), $error); } return $error; } // ... }
Converting a response to an instance of WP_Error
starts by decoding the response. It’s possible that decode_response
creates an instance of WP_Error
. If that happens, we want to return that error instead of the empty default error.
If we get an array, we want to merge that array of error codes and messages into our empty default error. I do this using a PHP function called array_walk
. Using it allows me to extract the logic for adding an error to the instance of WP_Error
. For each element of the array, array_walk
will call another function add_response_error
.
class WP_API_Client { /** * Adds the response error to the given WP_Error instance. * * @param mixed $response * @param mixed $key * @param WP_Error $error */ private function add_response_error($response, $key, WP_Error $error) { if (!is_array($response)) { return; } $error->add( isset($response['code']) ? $response['code'] : '', isset($response['message']) ? $response['message'] : '' ); } // ... }
This method’s purpose is to attempt to validate and add part of a response into an instance of WP_Error
. Before doing anything, it checks that we have an array. The expected array will have two keys:
- code
- message
We use the ternary operator to prevent PHP warnings. The ternary operator returns an empty string if either keys isn’t set.
Final notes
Holy moly, that was a lot! Like I said in the limitations, there was plenty for you to cover. Let’s go over a few final points.
Power of object-oriented programming
The goal of this design example was to show you the power of using objects for solving complex problems. That said, you were also able to see the extra work that you needed to do when you didn’t.
If WordPress core used more classes instead of arrays, you could avoid a lot of the work that we did. You could also cut a few internal methods like get_response_headers
, get_response_status_code
and is_successful
. You also wouldn’t need to do as much validation.
How you can proceed from here
I want to discuss how you could continue based on the work you saw here. We took time to break down a lot of the internal methods. This allows us to reuse them with the other API methods.
Let’s look at an example. Retrieving the current user would only need that you create a new public method and a new endpoint constant. Retrieving posts would be quite similar as well.
Creating a new user would be more work. You’d need to work create an internal method for POST requests. You’d have new status codes to support. New outcomes.
That said, the foundation you need is there to help you to move ahead.
The complete picture
You can find the documented code on a GitHub project I created for it. I plan on fleshing out the client in the future. So feel free to use it and check back on it.
I’ll leave you with an exercise. If you had to extract part of the class into a new one, what would you split out? Let me know in the comments!