Skip to content
← All posts

Design Patterns in WordPress Plugins, With Code Examples

The design patterns WordPress already uses, and the ones worth adding to your own plugins, with short PHP examples for each.

Business Coffee Composition Computer

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

Wow Themes Team

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.

ShareXLinkedInFacebook

Comments

No comments yet.

Comments are closed on this post.

Keep reading

Creating a WordPress website using Elementor

· 5 min read

Elementor for Beginners: Build a WordPress Page Step by Step

A plain-English Elementor guide for WordPress beginners: installing it, the settings worth changing, global styles, and the free widgets you'll use on almost every page.

8 Proven Ways You Can Try To Recover Your Hacked WordPress Site

· 5 min read

Hacked WordPress Site? How to Recover It, Step by Step

What to do in the first hour after a WordPress hack, how to restore or clean the site, and how to stop the same attacker getting back in.

coffee apple iphone smartphone

· 6 min read

How to Speed Up WordPress with LiteSpeed Server Caching

Measure your server response time, then set up LiteSpeed Web Server on Plesk, the LiteSpeed Cache plugin and the QUIC.cloud CDN, with screenshots of each step.