A design pattern is a reusable answer to a problem that keeps coming up. If you've written a WordPress plugin, you've already used several without naming them. This post shows the patterns WordPress itself is built on, the ones worth adding to your own plugins, and one that's often overused, with short PHP examples. The examples use PHP 8.0 syntax, which matches the minimum our own themes require.
Hooks are the observer pattern
WordPress is event-driven. Code announces that something happened (the publisher), and any other code can react (the subscribers). In WordPress, do_action() publishes and add_action() subscribes:
// Your plugin announces an event.
do_action( 'myplugin_order_exported', $order_id, $file_path );
// Any other plugin, or the site owner's code, can react to it.
add_action( 'myplugin_order_exported', function ( $order_id, $file_path ) {
error_log( "Order {$order_id} exported to {$file_path}" );
}, 10, 2 );
Filters are the same idea with a return value. Publish your defaults through a filter and other developers can change them without editing your files:
$columns = apply_filters( 'myplugin_export_columns', array( 'id', 'total', 'date' ) );
This is the single most useful habit for plugin authors: add your own actions and filters at the points where someone might want to change your plugin's behaviour. It's why WordPress is so extensible, and it costs one line each.
Singleton: common, and often a mistake
Many plugins wrap everything in a class that can only be created once:
final class My_Plugin {
private static ?My_Plugin $instance = null;
public static function instance(): My_Plugin {
return self::$instance ??= new self();
}
private function __construct() {
add_action( 'init', array( $this, 'register_post_types' ) );
}
}
My_Plugin::instance();
It stops hooks being registered twice, which is why it's popular. The cost is that every class reaches for My_Plugin::instance() from anywhere, which makes the code hard to test and hard to change. Our view: one singleton at the plugin's entry point is fine. Singletons inside the plugin, for the logger, the settings, the API client, are usually a sign that dependency injection would serve you better.
Dependency injection: pass in what a class needs
Instead of a class creating or fetching its own helpers, hand them in through the constructor:
interface Exporter {
public function export( array $rows ): string;
}
final class Csv_Exporter implements Exporter {
public function export( array $rows ): string {
$handle = fopen( 'php://temp', 'r+' );
foreach ( $rows as $row ) {
fputcsv( $handle, $row );
}
rewind( $handle );
return stream_get_contents( $handle );
}
}
final class Order_Export {
public function __construct( private Exporter $exporter ) {}
public function run( array $orders ): string {
return $this->exporter->export( $orders );
}
}
Order_Export doesn't care how the export is written. In a PHPUnit test you pass in a fake exporter, and nothing touches the file system.
Strategy and factory: let the format be swapped
Combine the interface above with a filter and you have the strategy pattern: the behaviour is chosen at run time, and other developers can plug in their own.
function myplugin_make_exporter(): Exporter {
$exporter = apply_filters( 'myplugin_exporter', new Csv_Exporter() );
return $exporter instanceof Exporter ? $exporter : new Csv_Exporter();
}
$export = new Order_Export( myplugin_make_exporter() );
myplugin_make_exporter() is a small factory: one place that decides which object to build. The instanceof check protects you when someone returns something unexpected from the filter.
Principles for code you'll maintain
- Readable: name things for what they do. The next developer, often you in a year, shouldn't need a comment to follow it.
- Reliable: the same input gives the same result. Tests with PHPUnit catch it when that stops being true; see our PHP testing and profiling tools.
- Reusable: don't repeat yourself. If you copy the same code to several sites, it belongs in a shared plugin.
- Extensible: add actions and filters where others will want to change behaviour.
For site builders: patterns without writing a plugin
Not everyone who builds WordPress sites writes code, but the same thinking applies:
- Research plugins before installing. Check the rating, the date of the last update and whether support threads get answers. Paid plugins usually come with direct support, which counts for a lot on client sites.
- Don't edit core, theme or plugin files. An update overwrites them.
- Use a child theme for template changes, so the parent theme can update safely. Our theme docs show the approach, for example keeping your changes safe in Kindle.
- Put snippets in a site-specific plugin, not in the theme's
functions.php. They then survive a theme change.
Where to go next
Pick one plugin you maintain, find the place where people most often ask you for a change, and add a filter there. If you build blocks as well as plugins, read fixing Gutenberg block validation errors.
If you sell through WooCommerce and want products that are not just "products", such as properties, courses or events, our WooCommerce Custom Post Type plugin lets you create, manage and sell them from your own custom post types.
Written by
The people behind Wow Themes
The Wow Themes team builds and supports WordPress themes and plugins, plus Vue and React templates. On the blog we write about building, running and securing websites: WordPress and WooCommerce, JavaScript, performance and the tools we use day to day.




Comments
No comments yet.
Comments are closed on this post.