How WordPress abstract classes keep your plugin code DRY

I was digging into a new client’s plugin last week. They had a handful of admin pages for different settings, and the request was simple: add a new configuration option to every one of them. Easy enough. But when I opened the code, I found the same 150 lines copy-pasted across six files, with only a few variables changed. What should have been a ten-minute job turned into an hour of careful search-and-replace. This is the kind of mess you get when you’re not using WordPress abstract classes to keep your code DRY (Don’t Repeat Yourself).

My first instinct was to just do the grunt work: copy, paste, bill for the hour. But that’s a junior move. You aren’t only fixing the problem in front of you, you’re leaving a landmine for whoever touches it next, which is usually your future self. The better fix is architectural. Get rid of the repetition.

Why abstract classes are your best friend here

An abstract class is a blueprint for other classes. It lets you define the common methods and properties a group of similar classes will share, but you can’t instantiate the abstract class itself. It forces a contract: any class that extends it must implement the specific “abstract” methods you define. For our admin pages, every page needs to register itself, render some HTML, and have a title. There’s the blueprint. The underlying idea is polymorphism, which I saw explained well on Carl Alexander’s blog, and that’s what prompted this post.

Take the problem itself. You might have a ShortcodePage class and an OptionPage class that look almost identical. Both register hooks, set options in a constructor, and render the page. It’s 90% the same code.

The “blueprint” abstract class

Instead of that duplication, we create one parent class to hold all the shared logic. Anything unique to each child class, like the page title or the actual form fields, becomes an abstract method. That’s the contract.

namespace MyPlugin;

/**
 * Base class for all WordPress admin pages.
 */
abstract class AbstractAdminPage
{
    /**
     * My plugin options.
     * @var array
     */
    protected $options;

    /**
     * Register the admin page with WordPress hooks.
     */
    public static function register()
    {
        // Late static binding in PHP 5.3+
        $page = new static(get_option('my_plugin_options', []));

        add_action('admin_init', [$page, 'configure']);
        add_action('admin_menu', [$page, 'add_admin_page']);
    }

    /**
     * Constructor.
     */
    public function __construct(array $options)
    {
        $this->options = $options;
    }

    /**
     * Adds the admin page to the menu.
     */
    public function add_admin_page()
    {
        add_options_page($this->get_page_title(), $this->get_page_title(), 'install_plugins', $this->get_menu_slug(), [$this, 'render']);
    }

    /**
     * Configure the admin page using the Settings API. (The Contract)
     */
    abstract public function configure();

    /**
     * Renders the admin page. (The Contract)
     */
    abstract public function render();

    /**
     * Get the admin page menu slug. (The Contract)
     * @return string
     */
    abstract protected function get_menu_slug();

    /**
     * Get the admin page title. (The Contract)
     * @return string
     */
    abstract protected function get_page_title();
}

Now look how clean the page classes get. Each one extends the abstract class and fills in the blanks the contract defines. There are no repeated hook registrations or constructor logic, only the code that makes each page unique.

namespace MyPlugin;

/**
 * Options page.
 */
class OptionPage extends AbstractAdminPage
{
    public function configure()
    {
        register_setting($this->get_menu_slug(), 'my_plugin_options');
        add_settings_section('my_plugin_options', __('Options', 'my_plugin'), null, $this->get_menu_slug());
        // ... add fields
    }

    public function render()
    {
        echo '<h1>' . $this->get_page_title() . '</h1>';
        // ... render form
    }

    protected function get_menu_slug()
    {
        return 'my_plugin_options';
    }

    protected function get_page_title()
    {
        return __('My Plugin Options', 'my_plugin');
    }
}

So what’s the point?

This isn’t about looking clever. It’s about working like a professional. Take that original client task: with this structure, adding a new option would have taken minutes. You’d change the one or two child classes that needed it and be done, with no hunting through files and no risk of missing one. The codebase stays predictable and, more to the point, maintainable.

  • Your logic lives in one place. That’s where you fix bugs and make updates.
  • Consistency is guaranteed. Every admin page must implement the required methods.
  • It saves time. The next similar task starts from a solid foundation.

This stuff gets complicated fast. If you’re tired of debugging someone else’s mess and just want your site to work, drop my team a line. We’ve probably seen it before.

Next time you catch yourself copying a class just to change a few lines, stop and ask whether there’s a blueprint underneath. Your future self will thank you.

author avatar
Ahmad Wael
I'm a WordPress and WooCommerce developer with 15+ years of experience building custom e-commerce solutions and plugins. I specialize in PHP development, following WordPress coding standards to deliver clean, maintainable code. Currently, I'm exploring AI and e-commerce by building multi-agent systems and SaaS products that integrate technologies like Google Gemini API with WordPress platforms, approaching every project with a commitment to performance, security, and exceptional user experience.