SponsoredHosinger Special Offer

WP+ WordPress Plugin Development

Build WordPress plugins from scratch: actions and filters, shortcodes, settings pages, custom post types, meta boxes, AJAX, REST endpoints, custom tables, cron and blocks.

Object-Oriented Plugin Architecture

WordPress Plugin Development Lesson 25 of 27 ~4 min read

Overview

As a plugin grows past a few hundred lines, organising it into namespaced classes — each responsible for one area, loaded by an autoloader and wired together by a single bootstrap — keeps it understandable, testable and free of naming collisions.

From prefixed functions to namespaces

Everything so far used the gdc_books_ prefix to avoid collisions. A PHP namespace does the same job for classes, more cleanly. GDC\Books\PostTypes can never clash with another plugin's PostTypes.

The structure

gdc-books/
├── gdc-books.php             Header + bootstrap only
├── composer.json
└── src/
    ├── Plugin.php            Wires every component together
    ├── PostTypes.php         Book post type + taxonomies
    ├── Admin/
    │   ├── SettingsPage.php
    │   └── MetaBoxes.php
    ├── Rest/
    │   └── BooksController.php
    └── Shortcodes.php

Autoloading with Composer

{
    "name": "gizdevcraft/gdc-books",
    "autoload": {
        "psr-4": {
            "GDC\\Books\\": "src/"
        }
    }
}

Run composer dump-autoload. Now new GDC\Books\PostTypes() loads src/PostTypes.php automatically — no more lists of require_once.

A focused component class

<?php
namespace GDC\Books;

defined( 'ABSPATH' ) || exit;

/**
 * Registers the Book post type and its taxonomies.
 */
final class PostTypes {

    public const POST_TYPE = 'gdc_book';

    public function register(): void {
        add_action( 'init', array( $this, 'register_post_type' ) );
        add_action( 'init', array( $this, 'register_taxonomies' ) );
    }

    public function register_post_type(): void {
        register_post_type(
            self::POST_TYPE,
            array(
                'labels'       => array( 'name' => __( 'Books', 'gdc-books' ) ),
                'public'       => true,
                'has_archive'  => 'books',
                'show_in_rest' => true,
                'supports'     => array( 'title', 'editor', 'thumbnail', 'excerpt' ),
            )
        );
    }

    public function register_taxonomies(): void {
        register_taxonomy(
            'gdc_genre',
            self::POST_TYPE,
            array(
                'hierarchical' => true,
                'show_in_rest' => true,
            )
        );
    }
}

The constructor does nothing. All hooking happens in register(), which the bootstrap calls. That separation means a test can create the object without side effects.

The plugin class and bootstrap

<?php
namespace GDC\Books;

final class Plugin {

    /** @var object[] */
    private array $components;

    public function __construct() {
        $this->components = array(
            new PostTypes(),
            new Shortcodes(),
            new Rest\BooksController(),
        );

        if ( is_admin() ) {
            $this->components[] = new Admin\SettingsPage();
            $this->components[] = new Admin\MetaBoxes();
        }
    }

    public function run(): void {
        foreach ( $this->components as $component ) {
            $component->register();
        }
    }
}
<?php
/**
 * Plugin Name: GDC Books
 * Version:     2.0.0
 * Requires PHP: 7.4
 * Text Domain: gdc-books
 */

defined( 'ABSPATH' ) || exit;

require_once __DIR__ . '/vendor/autoload.php';

add_action(
    'plugins_loaded',
    static function () {
        ( new GDC\Books\Plugin() )->run();
    }
);

Guidelines

  • One class, one job. If a class name needs "And" in it, split it.
  • Prefer passing dependencies in over reaching for globals or singletons. A singleton is hard to test and hard to replace.
  • Keep hook callbacks public — WordPress calls them from outside the class.
  • **Ship vendor/** in the release zip (or use a simple spl_autoload_register() loader), because users do not run Composer.
  • When bundling third-party Composer packages, prefix their namespaces with a tool such as PHP-Scoper, or two plugins bundling different versions of the same library will collide.

Practice

  • Convert the post type code into GDC\Books\PostTypes with a register() method.
  • Set up Composer PSR-4 autoloading and a Plugin class that runs every component.
  • Move the settings page into Admin\SettingsPage and load it only in the admin.
Create a free account to save which lessons you've finished. Save my progress