My first exposure to the SOLID principles for object-oriented programming came from the senior developers at the WordPress agency I was working at. It was a bit of a big change to how I had normally approached object-oriented programming. I was used to objects encapsulating broad swaths of functionality related to a particular thing. SOLID demanded that my objects be smaller, more focused, and much more numerous.
SOLID was hard for me to get the hang of, and in reality, I never got the hang of it while I was working for the agency. It felt like overkill: why should I create five different classes and boilerplate a ton of infrastructure just to end up calling register_post_type
? Why go to all the trouble of dependency injection when the functions I need to call are right there?
A few months removed from agency work (and knee-deep in the Ruby world), I’m finally starting to get it. And in a way, I was right: it is overkill… if I’m only building a WordPress plugin.
But Smolblog is not only a WordPress plugin.
SOLID-ly Overkill
SOLID is an acronym which stands for five principles of object-oriented programming:
- Each class has a single responsibility.
- A class is open to use by others but closed to changes.
- A class can be replaced by any of its subclasses according to the Liskov Substitution Principle.
- A class' interface is a promise to other classes to behave a certain way.
- A class' dependencies should be given to it according to the other principles.
Points 2, 3, and 4 are ideas I take for granted. Classes have public and private properties and functions, and while the private aspects can change as much as anyone wants, the public aspects are a contract with the rest of the program. And interfaces and inheritance are amazing ways to keep similar logic in one place and change things where they’re different. I learned this back in my C++ days. No big.
It was the first and last points that tripped me up, badly. My idea of a single responsibility was “This class handles everything to do with this thing.” The seniors said a single responsibility was “This class handles this one task.” I thought that sounded more like a function. I also struggled with dependency injection. What was the point of encapsulating logic inside of an object if you had to give the object the logic?
Trying to implement these principles just to create a post type simply wasn’t worth it. It made the code bigger and more complex than it needed to be. Combined with the fact that there were no real testing practices in place meant that trying to code fully-idealized SOLID code felt like all of the hassle and none of the payoff.
What the senior devs were aiming for was more than a couple of hooks; it was a future of much more complex functionality that would need to be picked up by a rotating squad of developers. It was a potential for writing tests on client-specific business logic.
SOLID principles aren’t overkill when you’re building an application; they’re essential.
Stop Trying To Do Everything
The first hurdle I had to get over was personal. I’m a people pleaser. I want to do everything for everyone all the time so that maybe people will like me. What I didn’t realize was that (toxic) idea had spread to my coding style: my “everything” classes were made in my own image.
I wanted to encapsulate logic into neat little packages that I could hide from the rest of the application. For example, I would want creating a new post to be (essentially) one line of code:
$post = new Post(
id: null,
title: 'My first blog post!',
author_id: 5,
content: 'This is so cool, I wish I knew how to blog.'
);
Behind the scenes, though, there would be too much happening:
class Post {
function __construct(
$id,
$title,
$author_id,
$content,
) {
global $db;
if (
!isset($title) ||
empty($title) ||
!isset($content) ||
empty($content) ||
!isset($author_id) ||
$author_id <= 0
) {
throw new Exception('Bad post data!');
}
if (isset($id)) {
$id = $db->query(
"UPDATE `posts` SET `title`=?, `author_id`=?, `content`=? WHERE `id`=?",
$title,
$author_id,
$content,
$id
);
} else {
$db->query(
"INSERT INTO `posts` SET `title`=?, `author_id`=?, `content`=?",
$title,
$author_id,
$content
);
}
$this->id = $id;
$this->title = $title;
$this->author = $db->query('SELECT * FROM `users` WHERE id=?', $author_id);
$this->content = $content;
}
// Other helper methods and such...
}
This pretend class is written with these requirements:
- Every
Post
object should correspond to something in the database.
- Every
Post
object should have a title, author, and content.
- Every
Post
object should have an ID; we can infer that a new post will not have an ID and get one when the post is created in the database.
Right off the bat, though, we’ve coded some big assumptions into our class:
- The global
$db
object exists.
- The global
$db
object has a query method.
- Posts are stored in the
posts
table.
- Authors are stored in the
users
table.
Here’s the thing that took me so long to grok: even though these assumptions are probably true now, they may not be true later. On some level I understood this, but I figured if that day came I would spend a day pouring through the codebase making the necessary changes.
People pleaser, remember?
If we were to make this code more SOLID, we’d have a few different classes. First, we’ll pare down the Post
class to just one responsibility: data integrity. If the class is given bad data, it should not create the object. So now our class can look like this:
class Post {
function __construct(
?int $id = null,
string $title,
Author $author,
string $content,
) {
if (
empty($title) ||
empty($content)
) {
throw new Exception('Bad post data!');
}
$this->id = $id;
$this->title = $title;
$this->author = $author;
$this->content = $content;
}
// Get/set methods...
}
Not only did we take out all the database code, we also added type hints to the constructor’s parameters. This way, PHP itself can check if title
, author
, and content
are set and throw an error if not.
Saving $post
to the database and turning some author_id
into an Author
object with data are not the responsibility of a Post
.
Creating a Dependency
Let’s go back to our hypothetical post creation and put that code in context. We’ll say we’re getting a request through the API to create a new post. With our old do-everything Post class, that endpoint class could look like this:
class NewPostApiEndpoint {
public function __construct() {}
public function run(WebRequest $request) {
$post = new Post(
id: $request['id'] ?? null,
title: $request['title'],
author_id: $request['author_id'],
content: $request['content'],
);
return new WebResponse(200, $post);
}
}
Short, sweet, and to-the-point. Beautiful. Except now we know what horrors once lied beneath that innocuous new Post
call. We could bring all those database calls into our endpoint class, but that wouldn’t fix the underlying issue: what happens when the database needs to change?
Really, the first question we should ask is, “What is the responsibility of NewPostApiEndpoint
?” Our short-and-sweet class helps us answer that question: to save a Post
with the data from the web request.
What’s not included: knowing how the Post
is stored. “But we know it’s a database!” Yes, we know it’s a database; the class should only know what it needs to do its job. So let’s start writing our new endpoint but leave comments where we have missing information:
class NewPostApiEndpoint {
public function __construct() {}
public function run(WebRequest $request) {
$post = new Post(
id: $request['id'] ?? null,
title: $request['title'],
author: // TODO We have author_id, need object
content: $request['content'],
);
// TODO Have post, need to save
return new WebResponse(200, $post);
}
}
We’ve identified two outside responsibilities: getting an Author object and saving a Post object. Those sound like single responsibilities to me!
Here’s where the power comes in: our endpoint object doesn’t need a specific object for these jobs, just an object that can do the job. So instead of writing new classes, we’ll create two interfaces:
interface AuthorGetter {
public function getAuthor(int $author_id): Author;
}
interface PostSaver {
public function savePost(Post $post): void;
}
Now that we have those interfaces defined, we can finish our endpoint:
class NewPostApiEndpoint {
public function __construct(
private AuthorGetter $authors,
private PostSaver $posts,
) {}
public function run(WebRequest $request) {
$post = new Post(
id: $request['id'] ?? null,
title: $request['title'],
author: $authors->getAuthor($author_id),
content: $request['content'],
);
$posts->savePost($post);
return new WebResponse(200, $post);
}
}
And that’s Dependency Injection in a nutshell! Cool, right?
Yeah…except, again, we’ve only moved the complexity. We still have to make those database calls at some point. And when it comes time to finally assemble the application, we have to keep track of what classes have which dependency, and… ugh.
Can’t See the Trees For the Forest
This was my other problem with the SOLID principles: how is Dependency Injection supposed to actually make things easier? I understood the idea of passing in what an object needs, but I got overwhelmed trying to picture doing that for an entire application. Trying to keep track of all of all the dependencies for an object also meant keeping track of those dependencies' dependencies, and it didn’t take long for the infinite recursion to crash my brain.
What I failed to grasp was that knowing objects' dependencies counts as a single responsibility. So why not let computers do what they do best?
The established pattern here is known as a Dependency Injection Container. The PHP Framework Interop Group has an established interface for these containers that is widely accepted across the industry. These objects store a mapping of classes and dependencies and create properly-initialized objects.
To complete our example, we’ll use the Container package from The League Of Extraordinary Packages:
use League\Container\Container;
$container = new Container();
$container->add(NewPostApiEndpoint::class)
->addArgument(AuthorGetter::class)
->addArgument(PostSaver::class);
// Later on...
$newPostEndpoint = $container->get(NewPostApiEndpoint::class);
And that’s pretty much it! We set up our classes to accept the dependencies they need, then we set up the container to get those dependencies to the classes. If those dependencies have dependencies, the container will take care of them too.
The only thing we have left to do is actually set up our two dependencies. We added the interfaces as arguments, but we haven’t given any concrete implementations to the container. We’ll skip writing out those classes and just show how it could work here:
$container->add(AuthorRepo::class)
->addArgument(DbConnector::class);
$container->add(PostRepo::class)
->addArgument(DbConnector::class);
$container->add(AuthorGetter::class, AuthorRepo::class);
$container->add(PostSaver::class, PostRepo::class);
This tells our container that that anything that depends on AuthorGetter
should be given an instance of AuthorRepo
, and anything that needs PostSaver
should be given PostRepo
.
But why go to all this trouble? We’ve taken a few lines of code and spread them out over three classes (five if you count the interfaces) and introduced an entirely new library to our code. While the individual pieces of the code may be easier to follow, the flow of code through the entire application is now much more complex. What does all this extra work actually get us?
Know What You (Don’t) Need
I could say “it makes things easier to change” and leave it at that. By isolating different responsibilities in the code, it’s easier to find and change that specific code when circumstances change (which they inevitably will). But this truth can be hard to visualize when those changes seem unlikely.
I’m writing about these principles because I’m using them to rewrite Smolblog. The version currently running here is a WordPress plugin top-to-bottom, albeit with some efforts at SOLID principles. It gets the job done, but it doesn’t feel maintainable. There’s a lot of code that should be abstracted out, but I didn’t see a good way to.
For the rewrite, my guiding principle was “leave your options open.” I wasn’t sure what was going to be best for Smolblog in the long term despite feeling very sure that WordPress was the best option in the short term. I didn’t want to box Smolblog into using WordPress longer than it needed to, but I also didn’t want to bet on a PHP framework only to discover it was a bad fit and have to rewrite large swaths of code again.
About a month into the rewrite I realized I had stumbled backwards into SOLID programming. In order to isolate my code from the outside platform, I had to write interfaces for anything I needed from the platform. I started with several overly-complicated setups before everything finally clicked enough to…
Well, it’s clicked enough that I finally feel confident enough to write this blog post and introduce you to Smolblog\Core. This is where all of the Smolblog-y logic will be that makes Smolblog what it is. And the single biggest change is that while the code still depends on an outside platform, that platform doesn’t have to be WordPress.
There’s more complexity here, sure. Version one has just under 1600 lines of code while Smolblog\Core has 1800, and it can’t do everything version one can yet! But with that added complexity comes a better separation of concerns.
In the container example above, I noted that we could define interfaces in one area then set the concrete implementations in another. That’s the principle I’m using to keep the WordPress-specific code separate from the core code. This way, the WordPress-backed PostSaver class might look something like this:
class WordPressPostSaver implements PostSaver {
public function savePost(Post $post): void {
wp_insert_post(
'id' => $post->id ?? 0,
'post_title' => $post->title,
// et cetera...
);
}
}
One that used the database directly could look more like this:
class DatabasePostSaver implements PostSaver {
//...
public function savePost(Post $post): void {
$this->db->query(
"INSERT INTO `posts` SET `title`=?, `author_id`=?, `content`=?",
$post->title,
$post->author->id,
$post->content
);
}
}
And because of the abstractions we’re using (namely the PostSaver
interface), nothing else has to change.
Smolblog is still being built on top of WordPress. This time, though, all of the WordPress-specific code is in its own project. All of the WordPress idioms and functions get passed through their own set of classes to match the interfaces that Smolblog requires.
Now, instead of different WordPress functions being sprinkled throughout the project, they’re centralized and cataloged. We know what Smolblog can do and what it needs WordPress (or some other framework) to do.
SOLID-ly understood
I genuinely think part of the reason SOLID never clicked for me at the agency was the simple fact that we were always going to be using WordPress. I didn’t see a difference between using a WordPress function and using a built-in PHP function; both were always going to be there, so why bother isolating them as a dependency? Now that I’m working on a project—a big one!—that doesn’t have that constraint, I’m beginning to see the value even if we were staying with WordPress.
I still maintain that if you know the plugin will never be more than a few calls to WordPress-specific functionality, like custom post types and taxonomies, then it’s best to use some namespaces and just get the job done. But I should also admit that it’s taken a lot of experience to know which is which.
It’s not lost on me that at some point in this project I’ll have 90% of a framework for using these principles in a WordPress plugin if not 90% of a framework in general. Combining these principles with Domain-Driven Design and Command-Query Responsibility Separation almost guarantees it… but that’s another blog post.
For now, I’ll just go ahead and admit it: y’all were right. As usual.