Modern approach to WordPress plugin development
Since the birth of WordPress, PHP has evolved significantly, integrating features that make it still a valuable technology for web development in 2024, also thanks to awesome frameworks like Laravel and Symfony.
WordPress is still a valuable option too, as a complete CMS and website-builder. However, its PHP coding standards regarding plugin development are stuck at ten years ago. In a nutshell: file naming conventions differ from the rest of the PHP development world, making PSR-4 autoloading impossible; namespaces are not encouraged: official documentation talks about avoiding naming collision, problem that was solved years ago with the introduction of namespaces; the plugin boilerplate recommended by the official documentation hasn’t been updated since February 2019, more than five years ago, and it encourages a file structure that splits business logic in three different directories: admin, includes and public, instead of keeping it in only one place and separating it inside the source code directory.
I think that WordPress plugin development needs a refresh, and not the one given by the advent of blocks, I’m talking about the good old PHP, which is now at version 8.3 and has a lot of game-changing new features.
For this reason, I would like to present my current approach to modern WordPress plugin development.
In Composer We Trust
We’re in 2024, you can’t do without Composer and the help it gives to PHP developers. After creating your new plugin directory, only populated by a fantastic <plugin-name>.php file, the first thing you’ll want to do is running
composer init
and following the project setup. You’ll then create an includes directory, that will be the home of all your business-level code, as well as the starting point for PSR-4 autoloading in your plugin. Add this part to your composer.json file, if you haven’t already:
"autoload": {
"psr-4": {
"Your_Company\\Plugin_Name\\": "includes/"
}
}
Underscores in namespaces and class-names should be preferred over PascalCase here in WordPress, since it’s a convention.
You can now install and use any package you want without worrying about files loading.
Singleton pattern
Your plugin, in OOP, can be seen as a singleton, a class instantiated only once that represents your whole plugin during the execution of your wonderful scripts.
Create a Plugin.php class inside the includes directory, that will look something like this:
<?php
namespace Company\Plugin_Name;
class Plugin {
protected static ?self $instance = null;
protected ?string $entry_point = null;
public static function get_instance(): self {
if ( is_null( self::$instance ) ) {
self::$instance = new self();
}
return self::$instance;
}
public static function run( string $entry_point ): self {
$plugin = self::get_instance();
$plugin->entry_point = $entry_point;
register_activation_hook( $entry_point, function () {
self::activate();
} );
register_deactivation_hook( $entry_point, function () {
self::deactivate();
} );
// Other initialization code...
return $plugin;
}
protected static function activate(): void {
flush_rewrite_rules();
}
protected static function deactivate(): void {
flush_rewrite_rules();
}
}
All the plugin-level code will be placed in this class, which will be initialized in the plugin entry point: <plugin-name>.php
<?php
/*
* Plugin Name: Plugin
* Description: This is a short description of what the plugin does. It's displayed in the WordPress admin area.
* Version: 1.0.0
* Author: Developer
* Text Domain: my-plugin
*/
require __DIR__ . '/vendor/autoload.php';
use Company\Plugin_Name\Plugin;
Plugin::run( entry_point: __FILE__ );
This will be the only code you’ll write in the plugin entry point file: its role is to start the execution of your plugin and move away business logic to your classes.
Don’t follow WordPress file naming rules
You may have noticed that the file containing the Plugin class is named Plugin.php, and not class-plugin.php. This is because of PSR-4 autoloading requirements; in fact, WordPress differs from the rest of the PHP world in terms of file naming conventions, and this is not good at all. So feel free to ignore this non-sense rule and name files containing classes with the name of the class (this also allows rich IDEs like PhpStorm to classify files as single-class files).
Same thing for the directories inside the includes folder: name them with capital first letters.
If you ever feel guilty for ignoring these WordPress conventions, remember that WooCommerce, plugin maintained by Automattic, none other than the company that maintains WordPress core, totally ignores any convention defined about file naming.
Use namespaces for isolated helper functions
You may want to define some helper functions in order to simplify frequent operations. Use namespaces as well for them:
<?php
namespace Company\Plugin_Name\Support;
use Company\Plugin_Name\Plugin;
function plugin_version(): string {
return Plugin::get_instance()->get_version();
}
In the case above, the code is placed inside an includes/Support/helpers.php file.
The function plugin_version() will be included as follows:
<?php
use function Company\Plugin_Name\Support\plugin_version;
This is really useful if you want to use generic names for your helper functions, because this way you won’t have to worry about prefixing your functions in order to avoid conflicts with other plugins.
I personally make a large use of this kind of functions in my plugins. My only rule is: don’t implement business logic into the functions themselves, but rather use them as wrappers for class methods.
Of course, in order to autoload the helper functions, you’ll need to specify the file path in composer.json as follows:
{
"autoload": {
"psr-4": {
"Your_Company\\Plugin_Name\\": "includes/"
},
"files": [
"includes/Support/helpers.php"
]
}
}
Keep a clean folder structure, with separation of concerns in mind
Try to give specific responsibilities to your classes and keep your codebase modular. Here is an example of a possible directory structure in your includes folder:
Of course, this is nothing more than an example, but it gives an idea of what a modern WordPress plugin’s structure should look like.
Use static code analyzers
Since you have included Composer in your project, you can also use development packages. Between them, there is a category of must-have tools: static analyzers like PHPStan or Psalm.
I personally use (and love) PHPStan, which can be easily integrated with WordPress by following this wonderful guide.
This kind of tools is really game-changing in terms of increasing the quality of the code you write, so I highly recommend using them as much as possible.
Use wp-env as your local development environment
With the advent of block themes, the Gutenberg team has developed a really handy local development tool based on Docker called wp-env.
Since I’ve started using it, I’ve completely abandoned Local, so I suggest you to give it a try.
Final thoughts
In conclusion, this approach is absolutely not opinionated, but I have been working this way for a long time, on different plugins, and the pattern is always the same. And it works! That’s the reason why I decided to write about it.
What do you think about these guidelines? Don’t hesitate to comment if you have any doubts or suggestions.
Thank you for getting this far!