This article on advanced OOP for WordPress was originally published by Torque Magazine, and is reproduced here with permission.
I’ve written a lot about object-oriented PHP and the WordPress REST API for Torque over the past few years. I’ve also touched on using Composer for dependency management and to provide an autoloader, as well as covered unit testing. The basic message of everything I’ve written is that by applying the established best practices of software development to how we develop for WordPress, we can create better plugins.
This is the first of a series of articles that will pull together these concepts in a practical, functional example. I’ll be walking through creating a WordPress plugin to modify the capabilities of WordPress REST API endpoints so they can be better optimized for search. The plugin is available on GitHub. You may want to browse the commit log to see how I put it together.
In this series, I’ll cover structuring plugins and classes using modern object-oriented PHP and not only how to make it testable, but also how to write automated tests for it. I will cover the difference between unit tests, integration tests, and acceptance tests and show you how to write and automate running each type. This article begins the series by showing how to use filters to modify the WordPress REST API using an object-oriented approach.
Improving WordPress Search Using the REST API
Plugins like SearchWP or Relevansi, or integrations with ElasticSearch — a technology that uses a totally different stack than WordPress — using Jetpack or ElasticPress, are often used to improve WordPress search. These types of plugins provide better search results and often pair well with faceted-search interface, which is great for eCommerce apps.
Search via the WordPress REST API inherits all of these same problems and the same solution. In this post, I’m going to start by looking at how search works by default, and what the limitations are. Then we’ll look at how to modify the search using two different methods and integrate with SearchWP.
WordPress’s built-in search capabilities often need to be improved using outside services. While this article is about an object-oriented approach to modifying how WordPress REST API routes for posts work, the practical example will be improving search.
When WordPress is used as the back end for a decoupled front end such as a native mobile app or web app, probably built using Vue or React or Angular, having quality search via the REST API is important. The code this article covers will help you if your app’s users need to find the right product variation or search content by a complex algorithm based on multiple taxonomies, and you’re writing custom code, not just installing a plugin.
Searching Posts with the WordPress REST API
If you wanted to search for all posts that were of the post type “product” on a site, using the search terms “Taco Shirts” you would make a request to the /wp/v2/product?s=Taco+Shirt
endpoint. If you wanted to improve the quality of the results, the solutions I listed above would help.
As we discussed above, WP_Query
, what the post endpoints of the WordPress REST API use, is not a great tool for search. More specifically, WP_Query
, probably due to its dependence on MySQL, is inferior to specialized search tools that tend to be built using NoSQL databases.
First, let’s look at how we can bypass WP_Query
’s interactions with WordPress’s database if a REST API request is being made.
This is the strategy many search plugins take to substitute the results of their own search systems, for what WP_Query
would have generated by default. The search system may use the same database. It may also connect to some other database, possibly via an API request, for example to an ElasticSearch or Apache Solr server.
If you look in WordPress core, you’ll find the filter “posts_pre_query” runs right before WP_Query
queries the database, but after the SQL query has been prepared. This filter returns null by default. If that value is null, WordPress continues with its default behavior: querying the WordPress database and returning the results as a simple array of WP_Post
objects.
On the other hand, if the return value of this filter is an array — hopefully containing WP_Post
objects — then WordPress’s default behavior is not used.
Let’s look at how we can use posts_pre_query to return a mock WP_Post
. This strategy is very useful for testing, but a more complex version of the same pattern can be used to integrate a separate database with your WordPress site:
/**
* Replace all WP_Query results with mock posts
*/
add_filter('posts_pre_query',
function ($postsOrNull, \WP_Query $query) {
//Don't run if posts are already sent
if (is_null($postsOrNull)) {
//Create 4 mock posts with different titles
$mockPosts = [];
for ($i = 0; $i <= 3; $i++) {
$mockPosts[$i] = (new \WP_Post((new \stdClass()))); //Fake post for demonstration, could be any WP_Post
$mockPosts[$i]->post_title = "Mock Post $i"; //Fake title will be different for each post, useful for testing.
$mockPosts[$i]->filter = "raw"; //Bypass sanitzation in get_post, to prevent our mock data from being sanitized out.
}
//Return a mock array of mock posts
return $mockPosts;
}
//Always return something, even if its unchanged
return $postsOrNull;
},
//Default priority, 2 arguments
10, 2
);
In this example, we’re using mock data, but we could be using SearchWP’s query class, or anything else. One other thing to keep in mind about this code is it will run on any WP_Query
, not just a WP_Query
object created by the WordPress REST API. Let’s modify that so we don’t use the filter unless it is a WordPress REST API request by adding conditional logic:
<?php
/**
* Replace all WP_Query results with mock posts, for WordPress REST API requests
*/
add_filter('posts_pre_query',
function ($postsOrNull, \WP_Query $query) {
//Only run during WordPress REST API requests
if (defined('REST_REQUEST') && REST_REQUEST) {
//Don't run if posts are already sent
if (is_null($postsOrNull)) {
//Create 4 mock posts with different titles
$mockPosts = [];
for ($i = 0; $i <= 3; $i++) {
$mockPosts[$i] = (new \WP_Post((new \stdClass()))); //Fake post for demonstration, could be any WP_Post
$mockPosts[$i]->post_title = "Mock Post $i"; //Fake title will be different for each post, useful for testing.
$mockPosts[$i]->filter = "raw"; //Bypass sanitzation in get_post, to prevent our mock data from being sanitized out.
}
//Return a mock array of mock posts
return $mockPosts;
}
}
//Always return something, even if its unchanged
return $postsOrNull;
},
//Default priority, 2 arguments
10, 2
);
Modifying WordPress REST API Endpoints Arguments
We just looked at how to change how the search results are generated for WordPress REST API requests. That allows us to optimize our queries for better search, but it is likely to expose a need for a different schema for the endpoints.
For example, what if you wanted to allow the search on your products endpoint to optionally allow additional post type to be included in the search. I covered a different approach to the same problem last year.
Cross-Cutting Concerns
We are about to look at how to modify the allowed endpoint arguments as well as how they are used to create WP_Query
arguments. That’s two separate concerns, and the single responsibility principle says we need one class for each concern. But both classes will have shared concerns.
For example, if we want to allow querying by different post types, we need to know what are the public post types, and what their slugs and rest_base arguments are. This is all information we can get from the function get_post_types
.
The output of that function is not exactly what we need. So let’s design a class to format the data according to the needs I just listed and give us helper methods to access it.
Think of it as one common shape for all of the post type data we need in a useable container:
<?php
/**
* Class PreparedPostTypes
*
* Prepares post types in the format we need for the UsesPreparedPostTypes trait
* @package ExamplePlugin
*/
class PreparedPostTypes
{
/**
* Prepared post types
*
* @var array
*/
protected $postTypes;
/**
* PreparedPostTypes constructor.
* @param array $postTypes Array of post type objects `get_post_types([], 'objects')`
*/
public function __construct(array $postTypes)
{
$this->setPostTypes($postTypes);
}
/**
* Get an array of "rest_base" values for all public post types
*
* @return array
*/
public function getPostTypeRestBases(): array
{
return !empty($this->postTypes) ? array_keys($this->postTypes) : [];
}
/**
* Prepare the post types
*
* @param array $postTypes
*/
protected function setPostTypes(array $postTypes)
{
$this->postTypes = [];
/** @var \WP_Post_Type $postType */
foreach ($postTypes as $postType) {
if ($postType->show_in_rest) {
$this->postTypes[$postType->rest_base] = $postType->name;
}
}
}
/**
* Convert REST API base to post type slug
*
* @param string $restBase
* @return string|null
*/
public function restBaseToSlug(string $restBase)
{
if (in_array($restBase, $this->getPostTypeRestBases())) {
return $this->postTypes[$restBase];
}
return null;
}
}
Notice that we didn’t call get_post_types()
in the class, instead, we used it as a dependency, injected through the constructor. As a result, this class can be tested without loading WordPress.
This is why I would describe this class as “unit testable”. It relies on no other APIs and we are not worried about side effects. We can test it as one single, isolated unit. Separating concerns and isolating functionality into small parts makes the code maintainable, once we have unit test coverage. I’ll look at how to test this kind of class in my next post.
Keep in mind that this class does rely on WP_Post_Type
. My unit tests will not have that class defined, as only integration tests will have WordPress or any other external dependency available. That class is only used to represent data, not to perform any operations. We can, therefore, say its use creates no side effects. As a result, I am comfortable using a mock in place of the real WP_Post_Type
in the unit tests.
Speaking of dependency injection, the classes that require objects of this new class, we want to follow the same pattern. Instead of instantiating PreparedPostTypes
inside of the classes that need them, we will pass in an instance. This means the classes consuming PreparedPostTypes
and PreparedPostType
remain isolated and can be tested separately.
It could also lead to code reuse as we have to make that dependency injection possible and have a property for that object. We could use cut and paste, or we could use a PHP Trait, which is a fancy more scalable way to copy methods and properties between classes.
Here is a Trait that establishes a pattern for how we inject the PreparedPostTypes
object into other classes
<?php
/**
* Trait UsesPreparedPostTypes
* @package ExamplePlugin
*/
trait UsesPreparedPostTypes
{
/**
* Prepared post types
*
* @var PreparedPostTypes
*/
protected $preparedPostTypes;
/**
* UsesPreparedPostTypes constructor.
* @param PreparedPostTypes $preparedPostTypes
*/
public function __construct(PreparedPostTypes $preparedPostTypes)
{
$this->preparedPostTypes = $preparedPostTypes;
}
}
Our other concern is we need to know some things about a post type in multiple places. For example the post type’s slug. This is a different flavor of a cross-cutting concern than the previous one. The last problem we solved involved dynamic data. Now we just need a single place to change a string or two we use in multiple places.
A class that has class constants solves this for us simply:
<?php
/**
* Class PostType
*
* Post type whose POST wp/v2/<post-type-rest_base> we are hijacking
*
*/
class PostType
{
/**
* Post type slug
*
* @TODO Change this to your post type's slug
*/
const SLUG = 'post';
/**
* Post type rest_base
*
* @TODO Change this to your post type's rest_base
*/
const RESTBASE = 'posts';
}
Now we can keep these strings consistent throughout our code. This may seem like an unnecessary step. But my example code works for the posts post type. If you want to change what post type is used this class needs to change and nothing else needs to change. This is following Tom McFarlin’s preferred definition of the single responsibility principle when he writes, “A class should have only one reason to change.”
The post Advanced OOP for WordPress: Customizing REST API Endpoints appeared first on SitePoint.
No comments:
Post a Comment