Over the last few months I’ve put a lot of time into writing Ingot — an automated A/B testing and conversion optimization plugin for WordPress. Because Josh, I did a lot of other things at the same time — but Ingot was my biggest project.
Ingot is probably the biggest plugins, in terms of lines of PHP code, that I wrote. I’ve worked on bigger plugins, but never as the primary developer. Pods and Caldera Forms are bigger, but my contributions code-wise to those are not huge.
For Ingot I wrote all of the PHP code. Roy Sivan helped me build the admin interface, which is an Angular app. He was also a valuable sounding board and bug catcher for the rest of the plugin.
I wanted to share some of the lessons I learned, as a developer, writing a big plugin. Writing this plugin from scratch allowed me to put to test a lot of my personal opinions on WordPress development. I’m going to follow this article with one specifically about coding a plugin around the WordPress REST API and another with some lessons I learned as an entrepreneur from the process.
Separations of Concerns
Working with the WordPress REST API helps encourage separation of concerns because of the way defining routes breaks up the process of preparing, processing and responding to requests. In addition, knowing that some of your CRUD will happen via your REST API, but not means you either have to wrap your API callbacks around a standard set of CRUD classes, or you need to keep two ways of reading/ writing/ validating and sanitizing data in sync.
The latter of those two options is insane. I went for the first. Ingot has a CRUD class for each of the different data types — test groups, variants, sessions, click tracking, settings — each of which extends a base class. Having separate CRUD class keeps the validation, sanitization and DB writing separated from every place that CRUD is ever used, including the REST API routes, and objects that represent groups or tests.
This also led to having one class to handle read/writes and establish a pattern for using CRUD, along with smaller extending classes to define what fields are needed. I liked working this way. Even though these classes are likely to be called multiple times, I used all static methods for them. Yes, there are objects for single groups, tests and sessions that are often needed, but CRUD is just a collection of functions and without any application for an iterator, having to instantiate multiple objects of these classes wouldn’t have provided any utility.
I think of these classes much like how WordPress core’s WPDB class, which might as well be all static methods since we always use one instance, relates to WP_Query. We use WPDB to inject post collections into specific WP_Query objects that we can then iterate over.
The way I worked wouldn’t have been doable without late static bindings. In non-static methods, $this refers to the current object. So if the class is extended, $this is the object of the extending class. The self keyword doesn’t work that way. self in the parent class refers to the parent class, not the subclass extending this.
On the long list of reasons why I didn’t hamstring myself with conventions of a language that is almost 8 years out of date (PHP 5.2) is late static bindings. Late static bindings allow for static inheritance. The keyword static functions like self, except it can refer to a subclass method or property if one exists.
One thing I’m really proud of in Ingot is that I stuck to very small classes. For example, I have a class that creates a conversion rates stats object. This is separate from the class that creates an object for stats for all of the variants in the group, which is a collection of the objects of my conversion rates stats class. This creates re-usable code — the conversion rate class is used for variant and group stats, easier testing, and avoids code reuse.
Small, reusable classes are not just neater and more testable, they make refactoring easier. I rewrote about half of Ingot in the week before release. Short story — CRUD was overcomplicated and the testing algorithm was going to require tons of traffic, which would be a problem for a big part of our target audience. That’s a different discussion, but my point for today is that small classes meant more saved code during that refactor since it was very easy to separate the parts that still had a job from those that didn’t.
This is a concern moving forward. I am considering whether it makes sense to break the plugin into smaller Composer packages that I can assemble into different, smaller, task specific versions. Doing so could also allow for implementing a CRUD system that uses the posts and postmeta table instead of custom tables.
Composer & Dependency Management
I used Composer for dependency management and providing a psr-4 autoload. There is a ton of blog posts out there on why I love Composer and dependency management in general. The PSR-4 autoloader forces a directory and class naming structure on the plugin that makes it highly organized. Between that logical organization and being meticulous about inline docs, I think the code is easy to follow. We will see as other developers get into it.
I did hit some stumbling blocks with dependency management. I used Composer’s file map for including PHP files that are not classes, which I found to be annoying. Classes are added automatically to the autoloader, since it is based on logic. File and class maps are static lists of files and adding to them requires a composer update, which I didn’t always alert Roy to, causing unneeded errors on his end. It also makes switching Git branches a pain.
My solution was to use bower-installer to copy all of the main files — IE the ones I needed — to a separate directory. This allowed me to exclude bower_components and all the extra stuff from the final zip.
I went on a bit of a quest to reduce the file size of the zip files I was creating for releases. Another big win was setting this command in my build script:
composer update --no-dev --optimize-autoloader --prefer-dist
This pulls in the Composer dependencies without their Git repos, or unit tests and optimizes the autoloader. Even with just a few dependencies, the file savings were pretty nice — about 6GB. Git repos are big.
Since the plugin is in a public Git repository I don’t think removing all this extra stuff that most end users don’t need does anyone a disservice or reduces their rights to modify the software. If an end user is a developer and needs the whole package, they can get it on Github.
I actually used very minimal third-party dependencies. But my testing library, the one I switched to later on, started out as a third party dependency. The library I used was built to use Redis to store data. So, at the least I needed to add a way to store data using my CRUD system.
At first I kept the testing library checked out from the original source — and that author merged my pull request to fix a bug I found. In order to facilitate this I kept my class to save data using my CRUD in Ingot instead of in the library.
As I found more places where my usage of that library wasn’t fitting well with the original design, I had to make more changes that didn’t make sense as pull requests. So, I started using my own fork and making changes directly there.
I will soon evaluate if my fork makes sense on its own, as a version of the original that stands on its own and is useful to have as a separate project or if it needs to be absorbed into Ingot fully. Either way, being willing to dive into a dependency, learn it, and modify was the difference between throwing it out and having to do another huge refactor and being able to ship the plugin on time(ish).
Iteration vs Throwing Shit Out
I had a lot of fun with this plugin. Working on something that doesn’t have any users yet is really nice. Once a project is released backwards compatibility and stability are concerns. When you’re iterating on something pre-release, you get to break stuff and start over.
It’s funny how quickly I can fall into the “fix it, don’t break it” mode out of habit that comes with maintaining plugins with lots of active users or contributing to WordPress. I literally had to tell myself a few times “don’t work around that bad design decision, you wrote it yesterday.”
As I said before, I re-did the base A/B testing algorithm pretty late in the process. This led to re-doing a lot of the CRUD and re-working the REST API and a lot of other parts of the plugin.
The lean startup mindset dictates the idea of getting to a minimal viable product quickly and improving it from there. WordPress preaches the idea of continual iteration. I agree with these mindsets and think that they work together well.
That said, neither of them can be an excuse for putting out something bad. When I started on Ingot I create a minimal viable A/B testing algorithm assuming I’d get it right — statistically speaking later — once I had everything else working.
This made sense to me, and once I had everything else working I sat down to write the code to do the math. I could have done it and I wrote a lot of it, but I realized that my approach was wrong. Not only that, I was adding it on top of some mistakes I made in the structure of my CRUD — separating price testing from content testing.
So, I spent “two days” throwing out half of the plugin and starting over. It was painful to throw out that much code — OK, I enjoyed it because Josh — but I’m glad that I corrected my mistakes before version 1.
There are somethings you can only do right, by doing them once in a way that is sub-optimal. Knowing when to throw out tons of code that works, has unit tests and documentation is hard. It’s not worth doing when it’s because you could have made the code cooler.
End-users don’t care about how fancy the code is, they care about results and they care if you can keep it working over time. Refactoring in order to meet your product design goals better, and to ensure your code will be more easily maintained over time is worth it.
Give It A Read
I hope you learned something from this post. I’ve resisted the urge to put in big code examples, as I wanted to keep it abstract. That said, you should feel free to read the source code. I’m happy to discuss these decisions I made, or how you can integrate with Ingot. Leave a comment or tweet at me.
Tomorrow I’ll post my feelings on what the WordPress REST API means for plugin development moving forward. It’s obviously something I’ve thought a lot about and putting my own theories into practice was one of the most valuable parts of this whole experience.