Backstory
In 2019 we got a big client that wanted a big shop on WordPress at my job, even though our agency only used Laravel, CodeIgniter, and React-Native at that time, and we did not have experience working with WordPress.
I was the guy with the most experience in WordPress at that time from personal hobby projects, so I was the main developer on that project, in which I learned a lot about WordPress.
Working with Laravel for a few years, I wanted to bring to WordPress some of the features of Laravel, like Blade templates, Eloquent models, and a Router.
Solutions
The solutions I found at the time are:
- Sage starter theme by Roots - this gives us the Blade template engine that we are familiar with from Laravel.
- TheUnderScorer/wp-eloquent - with this package installed in a custom WordPress plugin, in some parts of the project we used Eloquent ORM for general queries, in other parts we used the schema migration feature.
-
At the time I did not find or did not choose a solution for the router and controllers. We did not have any particular requirement for this; it was just a nice-to-have. So I made my own, using WordPress REST API features.
Looking back, Themosis framework would have been a way better choice that incorporates all previously mentioned features, plus more: middleware, helpers for any wp native feature, and Custom Post Types with already made field types. I would choose Themosis if I had to do this again in WordPress.
Even smarteist/wordpress-router deserves a mention here. It's a lightweight router.
Let's use awesome features from WordPress as well. The Gutenberg editor was new and shiny at the time, and I found a simple way to make custom blocks with the Block Lab plugin, now renamed to Genesis Custom Blocks.
Advices
A few pieces of advice when making a custom wp theme or plugin:
Don't do it ! (in certain scenarios)
First of all, if you know this website will need to scale to multiple millions of pageviews a month, explain to your client that WordPress will eventually become very hard to scale and maintain and try to explore other options like:
-
WordPress headless (build the frontend in a different stack and only pull data from WordPress)
I will present a solution for this in a future post using Solid-Start.
-
Laravel with an administration panel (Laravel Nova, Backpack for Laravel, Filament Admin are some of the top choices)
I will present my plugin customberg/customberg-php in a future post, which makes it easy to create and deploy custom Gutenberg plugins.
Refactor functions.php
Don't put everything in a single functions.php file!
Instead, you can make a folder to organize your functions based on feature, and then load them manually or using a glob, in a try-catch block:
// automatically with glob:
foreach (glob(dirname( __FILE__ ) . '/functions/*.php') as $filename) {
try {
require_once $filename;
} catch (\Exception $e) {
echo 'Custom functions failed: ' . $filename . ' - ' . $e->getMessage();
}
}
// if you choose to include them manually, you can optionally enable
// them based on visitor IP/cookie (useful for testing in production)
try {
include dirname( __FILE__ ) . '/functions/newsletter.php';
} catch (\Exception $e) {
echo 'Custom functions failed: ' . $e->getMessage();
}
Too many custom plugins does not help
Don't make a custom plugin for every feature. It can cause problems when you want to use a function or helper from another custom plugin, and the benefits of custom plugins for every feature disappear because you end up with a plugin depending on another plugin.
SQL Indexes are essential
Always check database schema created by third-party plugins for valid indexes!
Not all plugin authors understand how to make efficient SQL indexes, and this will eventually cause you problems when you reach a certain scale.
If you want to understand how to make very efficient composite indexes here is a great talk at Laracon Online by Aaron Francis - Database performance for Application Developers
Sometimes you have to give up on plugin updates
When you are relying very much on a third-party plugin, be prepared to give up on updates. We end up always wanting more customization from plugins, and when we don't have any action or filters to use, we have to modify the plugin and disable updates by setting the plugin version to 100.
We had this happen also for plugin bugs or performance improvements.
Sometimes custom is just better
Sometimes custom integration with native WordPress functions instead of third-party plugins that do the same thing could be better in the long run. Because we did not create custom Gutenberg blocks using their native APIs, we relied on Block Lab too much, and now we have to migrate to Genesis Custom Blocks and individually test and maybe fix 57 blocks.
Update >> Test
Always test all your plugin customizations after every update.
We had a few things break: Woocommerce cart count (used in a page template, and a block), custom email BCC header attached to a Contact Form 7 form.
Prepare for cache from the start
For better caching try to use javascript for user-related html differences, like different add-to-cart buttons when the product is already in the cart, recently viewed products, etc. If you make these features using PHP, you can't cache those paged publicly, only privately (individual cache per user), which is not efficient at scale. Instead make an ajax request and change the html via javascript.
Experiment with scaling options
If you have problems when scaling up, try experimenting with different database solutions (like managed databases from a cloud provider), or different webserver and cache plugin combinations: like OpenLiteSpeed and LiteSpeed Cache, or Nginx and WP-Super-Cache, and also different settings on said plugins, for example for us Object cache using Redis from the LiteSpeed Cache plugin was generating a huge unnecessary load and making our shop load much slower.
Git
If you try to use Git, having it on the entire installation (the base path) might save you from a catastrophe.
Add to gitignore any temporary folder, uploads, cache.
Commit after every individual update on wp or any plugin, might help you rollback safely if the new version breaks your customizations (beware some plugins make database modifications as well)
Having separate long-lived branches for your dev environments will help you compare the different modifications all over the place (theme assets, templates, functions, custom plugins, custom blocks, etc)
Beware: Making a merge between two long-lived branches can be dangerous. If you have different versions of WordPress or plugins it's a no-no because of database modifications, every WordPress or plugin update can make database modifications that don't carry over via git.
The way we deploy features to production is by comparing the two branches via git (VS Code extension GitLens), and manually copying every modification.
Git for Security
What I meant earlier by saving you from a catastrophe is, websites often can get hacked for multiple different causes, some of which are not in your control: weak passwords, plugin vulnerabilities, brute force, and server misconfigurations.
Surely backups help here, but there is always that chance that a hack can be undetected for months, and because backups usually cover 1-2 weeks the backup can contain the virus as well pretty well hidden along WordPress core files.
The way git helps here is you just periodically check using git status. The only flaw to this strategy is if the virus hides inside folders included in gitignore, like uploads or temporary folders.
Security
Speaking of security, I can't endorse enough the plugin WordFence Security. Setup bruteforce permanent bans, two-factor authentication, and monitor successful login email alerts for IP locations that you don't recognize.
Changing the login URL and the default admin username are great additions, also disable XML-RPC, and, as always, only use strong randomly generated passwords.
Top comments (1)
Really well put