In my last post in this series on advanced PHP object-oriented programming for WordPress development, I walked through refactoring the low-level API of a plugin, using tests to guide the process and make sure everything works correctly. I say “low-level API” because the focus was on the internal of how the system will work. I didn’t cover too much of wiring things together. In this article, that’s where this post picks up.
The next step was to create a factory that takes an array of arguments for the REST API to add, and an array of post types — that auto-wires those filters.
Test-driven development (TDD) is the practice of writing tests for a change in a code base before actually writing the change. Doing so makes the test the specification that defines if the new feature/ bug fix/ whatever works or not. Existing tests also passing defines if the new feature/ bug fix/ whatever are going to create regression errors or not.
Switching to TDD on an existing code base is not easy. Let’s use this existing plugin to look at how that can work.
Evaluating Existing Tests
First, it’s important to see what existing tests do. Right now, we have tests that cover the shape of the query results, mainly that they are an array of <code>WP_Posts</code>. A similar test ensures that the REST API response has the right shape.
That’s all good but none of them cover the contents of those
WP_Posts. So now we’ve identified what is missing in our test suite. Now we’re prepared to write tests.
Starting From Mock Inputs
I started by creating an array of what I wanted the input of this factory class. I wrote that array in a test for the factory.
Now, I’m going to create a non-functional version of the factory, and create a test that checks its basic functionality. Here is an outline of a Search class that consumes the two interfaces and has getter functions:
That’s what my factory will be creating. Let’s start the factory with a method that takes the arguments to add and the post types to add it to and emits a Search:
Now we have enough to write to cover the return type of each of the defined public methods:
That’s not going to pass yet — it is calling methods we have not written the bodies of yet. Once we do add a body to these methods, we have a way of quantifying if they work already. But that’s getting ahead of us.
First, we need to make the Search class set everything up. Before that can happen, we need to know what it does. Search has the responsibility of gluing together the system. Therefore it does several tasks, but as long as its acting as the controller that dispatches other systems, based on logic defined in those other systems, it still only has one responsibility.
Let’s list those tasks it has to dispatch:
- It needs to consume an object implementing the <code>ModifySchemaContract</code> interface in order to modify the schema of certain REST API routes and know which routes to modify.
- It needs to consume an object implementing the <code>ModifyQueryArgsContract</code> interface in order to modify the white list of query arguments.
- It needs to connect to the WordPress Plugins API (hooks) to make these changes.
Let’s start with the last one — connecting to the Plugins API. That’s what we have the least control over and dictates what we have to work with. A lot of my concern about how to get all of the right dependent objects —
WP_REST_Request from core went away by letting the plugins API supply the data.
Search will need to add hooks, which we want to remove that. I added an interface to formalize a pattern for this since it’s likely to be reused.
Since my goal, with the last set of interfaces was to make the
filterQueryArgs() methods go away, I will add those methods to Search. I should be able to use the methods of those two interfaces that define what the arguments to provide on the filter are and whether to apply the filter to have one place that performs this task for every implementation.
Here is a class that could do that, except none of the methods have bodies:
But, we can write tests to show how those methods should work. In addition to the test I already have, I’ll add this one to test that the properties for the query arg and schema arg modifier classes are set. This covers those methods and makes them safe to use in other methods:
That does not cover whether those hooks are running and if they are running correctly. That’s two key requirements, four really since we have to hooks. Here are the tests to prove that for the modification of the query arg whitelist:
We also should have tests for the schema argument modification. This is a great opportunity to play along at home and try what you’ve learned. Put in a pull request, participate actively for maximum educational benefit.
Designing A Factory
Ok, now let’s start putting this together in our factory. Factories are where objects are assembled. Abstraction makes factories more efficient since the same tool can create different, but similar products thanks to the use of a common interface.
So, I saw an opportunity here to move a lot of the implementation of the query arg and schema arg modifier classes to abstract classes they inherit, so I could reuse that logic inside of the factory. Without that, I’d have to use cut and paste or maybe a trait to reuse that logic. Inheritance is the simplest solution though. It makes sense, I want one or more classes that implement the same interface and do basically the same thing with different implementation details. That’s what class inheritance is for.
Here is my abstract query arg modifier class:
And here is the abstract schema arg modifier class:
You probably noticed that class only implements the interface. That’s all I could make reusable at this stage, so that’s what it does. Empty abstract classes tend to accumulate reusable methods over time.
Now that we have these two classes, we can extend them inside of our factory. I chose to use anonymous classes that extend those two classes. This allowed me to keep all of the factory logic in the factory. Anonymous classes are like anonymous functions, they have no name and are defined as needed.
Anonymous classes can extend other classes, implement interfaces, and use traits. They represent their scope in a special variable $this and can take arguments through a magic
__construct() method. Just like classes.
For example, this anonymous class extends
WP_Query and re-defines the query method:
Our anonymous classes, that will go in
Factory::search() start out looking like this:
Both of them are going to take the
$postTypesToSupport array that is passed to the method they are in as a constructor dependency. That constructor doesn’t exist in the classes they are inheriting, so the anonymous classes need to declare a constructor.
Since this method is passed an array of arguments, its job is to loop through and collect them correctly inside of the two objects responsible for using WordPress filters to provide that data. Here is that loop:
Now that the shape of the slot that the pieces have to stick in is more clear, let’s start building the Search class this factory creates objects of.
This is not the full implementation, but it zooms in on the two callbacks for our hooks. In both cases, search is able to know if the filtering should happen based on if a method, defined on the interface that is type hinted to return a boolean is true or false. It is able to get the array of data to add on the filters using a method that is defined on the interface and type hinted to return an array.
This does require a second refactor to the two interfaces I showed earlier and kept saying we’d have to revisit. It is now time for that second refactor.
Here is the new interface for filtering query arguments:
And here is the new interface for filtering schema arguments:
This refactor removes the needed for these classes to actually hook into the WordPress plugins API. That’s all handled by the Search class now. Also, the <code>shouldFilter()</code> method has the same signature in both, which I like.
Since interfaces can inherit other interfaces, we could move that method to its own interface that both of these interfaces inherit. Doing so would mean each interface only created one requirement, which the Interface Segregation Principle tells us to. Instead, I’m just going to tell you it’s wrong so you can fix it and put in a pull request. Write a blog post about it and @ me on Twitter.
Using The Plugins API To Capture Dependencies
This factory can setup filtering of how the inputs, but not actually the filter we use to modify query results. Earlier I discussed how Tonya and I decided that the classes that are responsible for creating the results, would need to be made aware of at least
WP_Query, and possibly
In order to do that, I needed a reference to the current
WP_REST_Request. Since WordPress doesn’t have a service container to request it from my solution, which you can see in this commit is to capture the current request so it can be injected to content getter using the rest_pre_serve_request filter.
I did this inside of the
FilterWPQuery class. I need the
WP_Rest_Request inside of the
getPosts() method. I added a new captureRequest method to grab the request, put it in a static variable that I could use in getPosts()
I can still test this with unit tests, replacing the Plugin API with my mocks and my other tests are still valid.
My next article will be about making this system extensible The
FilterWPQuery class currently has an
init() I refactored it to be a setter method with a corresponding getter. That way
getPosts() acts as a dispatcher the last set
contentGetter wins. I do wish this router was more abstracted, for example, different implementations per post type.
Here is what the class looks like at this point:
Now everything in our plugin that we would need to swap out for a different search implementation is swappable and we have a factory to manage that. I’ll build on that in the next article.
All of the code in this article and the previous article was added to the example plugin via this pull request. There are more tests than I discussed in this article. Again, this plugin does not have 100% test coverage and doesn’t yet do what it should do. Tonya and I will be improving it as we write, you are invited to join in. I think contributing to this plugin will be a good way to apply what you are learning.
Join the conversation