The two things I write most about for Torque are object-oriented PHP and the WordPress REST API. In this article, I will bring them together to show you how to build out a collection of custom REST API routes while applying the principles of object-oriented PHP.
One of the great things about inheritance in object-oriented PHP is that it lets us share code between classes. There are a lot of ways to avoid repeating ourselves in our code. Inheritance is one of them and should only be used when two or more classes have similar purposes.
If you have a class that creates an admin page and one that manages searches, they might need to share some code but it doesn’t make sense to have them extend the same base class because they do something totally different. On the other hand, if you have a class to make REST API endpoints for products and one for generating REST API endpoints for documentation on those products, it would make sense that both classes should share a common base class.
In this article, you’ll learn how to design a system for building WordPress REST API endpoints, taking advantage of class inheritance and making use of visibility and other principles I’ve discussed previously. If you’re unfamiliar with custom REST API endpoints, then you should review The Ultimate Guide To The REST API before moving forward.
I also recommend looking at how post type routes are built in the REST API plugin, which makes great use of class inheritance to build out routes for each post type.
Designing The System
Structure
For a complete example of a REST API system, we have three general asks:
- Generating routes
- Creating responses
- Booting the system
When creating routes, by way of example, I’ll create an abstract class, for CRUD routes. I’ll also create a more generic interface, which those routes will implement. The reason for having an interface is that, if you want to add routes that are not great matches for that pattern, you shouldn’t force them into it.
At the same time, the class that boots the system needs to be able to expect a certain class structure of the route objects it is working with. So the method in the class that boots the system will be type hinted to accept only classes that implement that interface. It would have worked to make it accept classes that extend the CRUD base class, but then we would have lost a lot of flexibility, and would have had to rewrite the system if non-CRUD routes were added.
The third task is creating a response. For the sake of this example, I will just be creating a generic successful response class and an error class. They will extend WP_REST_Response and WP_Error respectively, and not do much else. But, because all of the routes will return these objects, later on if you need common functionality in your responses, no need to refactor, just add the methods to those classes.
In this example, I’m going to use PSR-4 directory structure and namespacing. What’s great about PSR-4 is that it makes it so your directory structure helps to define what your code does. I often start a project by just laying out the directories to help me visualize what I will need. Here is the structure for this REST API generator:
Start With The Contract
Interfaces create a contract that classes that implement them must follow. In this example, we are only going to use one interface, and it makes it so every class that implements it has to have a method called “add_routes” that accepts one argument called namespace.
As a result, we can reliably know that all objects that implement this have that method and can be used in a predictable manner. How they handle adding routes doesn’t matter. Our CRUD base route class will define one pattern. The classes that extend it should follow that pattern or they can override that method. Other classes can implement this interface as long as they can fit the terms of the agreement this interface defines.
Here is our interface, it is very simple:
interface route { public function add_routes( $namespace ); }
Creating An Abstract Class
Now let’s make an abstract class called crud, that implements this interface. Of course, it has to have a method called “add_routes” so start with that. The naming convention for the set of routes this is going to create is largely lifted from the core REST API plugin. It’s a good pattern for CRUD results and sticking to that core standard helps other WordPress developers make sense of our code.
Here is what that method looks like:
/** * @inheritdoc */ public function add_routes( $namespace ) { $base = $this->route_base(); register_rest_route( $namespace, '/' . $base, [ [ 'methods' => \WP_REST_Server::READABLE, 'callback' => [ $this, 'get_items' ], 'permission_callback' => [ $this, 'get_items_permissions_check' ], 'args' => [ 'page' => [ 'default' => 1, 'sanitize_callback' => 'absint', ], 'limit' => [ 'default' => 10, 'sanitize_callback' => 'absint', ] ], ], [ 'methods' => \WP_REST_Server::CREATABLE, 'callback' => [ $this, 'create_item' ], 'permission_callback' => [ $this, 'create_item_permissions_check' ], 'args' => $this->request_args() ], ] ); register_rest_route( $namespace, '/' . $base . '/(?P<id>[\d]+]', [ [ 'methods' => \WP_REST_Server::READABLE, 'callback' => [ $this, 'get_item' ], 'permission_callback' => [ $this, 'get_item_permissions_check' ], 'args' => [ 'context' => [ 'default' => 'view', ] ], ], [ 'methods' => \WP_REST_Server::EDITABLE, 'callback' => [ $this, 'update_item' ], 'permission_callback' => [ $this, 'update_item_permissions_check' ], 'args' => $this->request_args( ) ], [ 'methods' => \WP_REST_Server::DELETABLE, 'callback' => [ $this, 'delete_item' ], 'permission_callback' => [ $this, 'delete_item_permissions_check' ], 'args' => [ 'force' => [ 'default' => false, 'required' => false, ], 'all' => [ 'default' => false, 'required' => false, ], 'id' => [ 'default' => 0, 'sanatization_callback' => 'absint' ] ], ], ] ); }
Outside of that method this class will have three groups of methods. One group is the endpoint callbacks, one is permissions callbacks, and another is utility methods. I’ll walk through each group one by one.
Let’s start with the utility methods. These are three simple methods that we need to make sense of the rest of the code in this class. The first is a method called “not_yet_response.” This method returns a 501 “Not Yet Implemented” HTTP status code. All of our routes are going to, by default return this. That way the subclasses will be able to respond to all of the possible CRUD endpoints just by virtue of being there, and we can slowly add the functionality as needed.
That method looks like this:
/** * Return a 501 error for non-existant route * * @return response */ protected function not_yet_response() { $error = new error( 'not-implemented-yet', __( 'Route Not Yet Implemented :(', 'your-domain' ) ); return new response( $error, 501, [] ); }
You can see it makes use of the two response classes I mentioned before. I’ll show you how they work shortly.
The second method, “route_base” is a method to get the base for the route. If a class called “shoes” extended this class, this method would return “shoes.” You might have noticed this is used in the add_routes method to form the endpoint URLs.
The third method in this group is called “request_args.” This will define the endpoint arguments for this class. This method is declared abstract, so the subclasses will have to define a set of arguments via that method.
You might have noticed that these three methods are protected. This visibility level make sense as these are methods that are only to be used internally. I did not use private visibility because I want the flexibility to override them in subclasses.
The route_base method I would probably always override with a hard coded string. But I like having the method that figures out the base as a fallback.
If I was writing this class for use only in PHP7, I would make sure each of these three methods had a return type declared to ensure they always returned the correct data type. That way if they were overridden in a subclass, they would definitely return the right type.
The next group of methods are the methods the endpoint response methods. Every one of these methods returns the not_yet_response method. I explained this above. One other advantage of this is in the subclasses of this class the inline doc can use @inheritdoc instead of duplicating the same inline docs everywhere.
One other thing to keep in mind about these methods is that they are public. They have to be because they are called by the WP_REST_Server class. Any other visibility setting would generate an error. But we don’t want these methods used in any context besides responding to a REST API request. That is why I type hinted the argument they accept as an object of the WP_Rest_Request class. This means they can be used by the WP_REST_Server class, and I can pass mock requests in as part of my unit tests.
That last paragraph also applies to the third group of methods in this class, the permissions checks methods. These methods are used to determine permissions for each of the routes. I made the opinionated decision to declare the get_item and create_item methods abstract and to have the other methods return one of those two methods.
The logic behind that decision is that it force each subclass to define at least the general read and write permission. But, since the permission to read one item or many items is generally the same, there is no need to define that separately, but it is possible. The same goes for write routes. Subclasses must define a create item permission, but that will be the same for deleting and updating items unless set otherwise.
Yes, this could lead to a lot of subclasses adding the same permissions callbacks and for that reason you might wish to create abstract subclasses of this class that handle permissions. For example, here is a simple class called “public_route” that makes it so the read routes are public, but only admins can write data:
namespace josh\api\routes; abstract class public_route extends crud { /** * @inheritdoc */ public function get_items_permissions_check( \WP_REST_Request $request ){ return true; } /** * @inheritdoc */ public function create_item_permissions_check( \WP_REST_Request $request ){ return current_user_can( 'manage_options' ); } }
That’s the base CRUD class. You can see the whole thing here. After I cover the response classes. I will give some examples of how to extend this class to make actual route collections.
Responses
As I said before, this system doesn’t do much with the response classes. The error response class doesn’t even have a body. But by starting with these classes in place and using them instead of the core classes they extend, as this system grows it’s easy to add utility methods to these classes or override methods in the core classes and have them apply to all of the responses from this API.
Here is my main response class:
namespace josh\api\responses; class response extends \WP_REST_Response{ public function __construct( $data, $status, array $headers ) { parent::__construct( $data, $status, $headers ); if( empty( $data ) ){ $this->set_status( 404 ); } } }
As you can see this is pretty simple. It’s really just a start point. The one decision I made was to check if the data being returned was empty and if so, reset the status code to 404. That’s an opinionated decision, but in my experience it simplifies things.
As I said my error response just extends WP_Error and doesn’t do anything else. But, it’s a fair bet as I used this system, I’d want to add a functionality for all errors, so I want to start with my own REST API error class.
namespace josh\api\error; class error extends \WP_Error { }
Putting It Together
Now, to show how this can work, let’s make a class that extends “public_route” so we have all of the endpoints we need and the permissions checks in place. I’ll just be showing the endpoints for getting one item or many items.
In my example, I’ll show how to flesh out getting items of a post type called “my-product.” In this route, the database query, via WP_Query will be in the route. I would almost always abstract this into separate CRUD operations, not tied to the REST API, for use elsewhere in my plugin or app. That is true even if those CRUD operations were all wrappers for WP_Query, as it would make it easier for me to move my data away from posts and custom fields later. I’m a firm believer that creating a database abstraction from the start is essential, as Pippin Williamson here.
Because I don’t want my whole REST API to be tied to WP_Query, I did not put the prepare_item_for_response and prepare_items_for_response methods in the base class. The core REST API plugin does this for the post type routes. That makes sense in that context, but your custom API should be flexible in what database abstraction it uses. You might use posts, you might use a custom table, you might use another database system entirely. Those are decisions I like to keep out of the base classes.
Here are those methods, which as you can see are type hinted to expect WP_Query and WP_Post objects:
protected function prepare_item_for_response( \WP_Post $item ){ return [ 'name' => $item->post_title, 'description' => $item->post_excerpt, 'price' => get_post_meta( $item->ID, 'price', true ) ]; } protected function prepare_items_for_response( \WP_Query $query ){ $items = []; if( ! empty( $query->posts ) ){ foreach ( $query->posts as $post ){ $items[ $post->ID ] = $this->prepare_item_for_response( $post ); } } return $items; }
In this class, I added a query class, that just wraps WP_Query. The reason for this is that in the future I might subclass WP_Query or remove WP_Query and I want flexibility to easily do that. That and the single responsibility principle is why I made a separate method for creating arguments for WP_Query.
protected function query_args( $type = '', $page = 1 ){ $args = [ 'post_type' => $this->post_type, 'paged' => $page, ]; if( ! empty( $page ) ){ $args ['tax_query' ] =[ [ 'taxonomy' => 'product_type', 'field' => 'slug', 'terms' => $type, ], ]; } return $args; }
This class has a lot of small methods, that’s great as it is designed both to create a collection of endpoints for products, and also so it can serve as the base class for other, more specific endpoints. Here is a subclass that just changes the query_args and request_args class to make a set of endpoints called “shoes” that only returns products with the “shoe” terms of the “product-type” taxonomy.”
namespace josh\api\routes; final class shoes extends product { /** * @inheritdoc */ public function request_args(){ $args = parent::request_args(); unset( $args[ 'type' ] ); return $args; } /** * @inheritdoc */ protected function query_args( $type = '', $page = 1 ) { return parent::query_args( 'shoes', $page ); } }
In both methods I used the parent keyword to call the method being overridden and just customize its behaviour. This simplifies things and as opposed to cutting and pasting from the parent method, makes keeping future changes in sync quite easy.
Starting It Up
The Boot Class
That’s almost everything. The one thing I have not shown you is how to start up the system. This is fairly simple. There is one class that takes all of the objects of the route classes and calls the add_routes method. A method we will safely assume exists because of our interface.
This class is going to have a constructor that takes the API namespace that is applied to all routes. Having this as an argument means this same class can be used to load multiple sets of endpoints that might have different namespaces or different versions numbers appended to that namespace.
public function __construct( $namespace ) { $this->namespace = $namespace; }
The constructor does not actually take the routes. I designed it this way as we need to construct an array of objects that implement the interface. I could have made it so this class took an array in its constructor, and then checked the contents of that array. I thought that smelled bad, so instead I added a method to add routes to that array and type hinted its one argument.
public function add_route( route $route ){ $this->routes[] = $route; }
Because adding route objects to this class happens after the class is instantiated, I needed a method to loop through that array, instead of doing it in the constructor. That method just loops that array and calls the add_routes method on each of the objects.
public function add_routes(){ if( ! $this->booted && ! empty( $this->routes ) ){ /** @var route $route */ foreach ( $this->routes as $route ){ $route->add_routes( $this->namespace ); } $this->booted = true; } }
Note that I am using a property to prevent this method from adding the routes twice.
Base Composition
So far I have not hooked any of this code to any actions. As a result we have a totally isolated system that can be called in a variety of ways. It’s a safe assumption that we will want this code loaded on the rest_api_init hook.
Here is the base file for the plugin I built for this example, it register an autoloader and instantiates the route and boot classes at rest_api_init. If you are applying this system to a plugin or theme, this probably doesn’t make sense, but for a specific site or app, loading the API this way, in a mu-plugin might make perfect sense.
use \josh\api\boot; add_action( 'rest_api_init', function(){ //http://www.php-fig.org/psr/psr-4/examples/ spl_autoload_register(function ($class) { // project-specific namespace prefix $prefix = 'josh\\api\\'; $base_dir = __DIR__ . '/api/'; $len = strlen( $prefix ); if ( strncmp( $prefix, $class, $len ) !== 0 ) { return; } $relative_class = substr( $class, $len ); $file = $base_dir . str_replace( '\\', '/', $relative_class ) . '.php'; if ( file_exists( $file ) ) { require $file; } }); //make product route $product = new \josh\api\routes\product(); //OMG(s) shoes! Shoes! $shoes = new \josh\api\routes\shoes(); //make API go $api = new boot( 'store\v1' ); $api->add_route( $product ); $api->add_route( $shoes ); $api->add_routes(); });
It is worth noting might want it on other contexts. For example if we wanted to generate API responses without an HTTP request. One potential use for that is if we had a theme or other front-end interface that worked from a JSON object. On initial page load, we could call the route directly and print the JSON response, or pass it to wp_localize_script.
Starting With A System
I put all of this code up in a git repo. You could add it to your own project as is and add your own routes by extending the CRUD class and use it as-is. That said, I’d encourage you to fork it and make it work with your own needs.
More importantly, I encourage you to use this as an example of how to take different parts of your plugins, themes, sites and apps and create systems around common functionality. This article uses the REST API as an example because it’s useful and I’m familiar with the REST API. In addition, I learned a lot about how following these principles of object-oriented PHP make for an easy to understand, extensible, and testable system by reading the code for the REST API version 2.
I would encourage you to design different groups of functionality in your code this way. Too often I’ve found myself diving right into the code to accomplish a task. That makes sense on one level, I had a problem and then I solved it. But too often I then had a similar problem and I found myself working backwards to pull a lot of what I had done into abstract classes and interfaces and then start building systems around them.
Starting by defining the system forces you to think about flow. It force you to think about how something would ideally work. Most of the time, thinking in terms of repeatable systems, even if you only need it once, will lead to building better, more testable, and more flexible systems that needs less refactoring as the application grows.
2 Comments