Skip to content
William Alexander
  • Home
  • Case Studies
  • Personal Projects
  • Articles
  1. Home
  2. Articles
  3. Object-Oriented PHP for WordPress Developers
WordPress Enterprise

Object-Oriented PHP for WordPress Developers

Moving beyond procedural code to maintainable WordPress development

April 5, 2025 14 min read

Key Takeaways

  • OOP makes WordPress code more organized, testable, and maintainable
  • Classes encapsulate related functionality and prevent global namespace pollution
  • WordPress hooks work seamlessly with object-oriented approaches
  • SOLID principles apply to WordPress development just like any other PHP project
  • Start with simple classes and evolve toward more sophisticated patterns as needed
Overview

Why OOP for WordPress?

I remember the moment I decided to learn object-oriented PHP seriously. I was maintaining a plugin I'd written two years earlier—2,000 lines of procedural code with functions like my_plugin_do_thing() and my_plugin_do_other_thing(). I needed to add a feature and couldn't figure out my own code. Functions called functions that called other functions, state lived in globals, and everything was tangled together. That plugin worked, but extending it was painful.

WordPress grew up procedural. Functions in the global namespace, hooks everywhere, and a coding style that made sense when PHP 4 was current and WordPress was a simple blogging platform. But PHP has evolved dramatically, and modern WordPress development doesn't have to be stuck in 2003. Object-oriented programming brings structure to complexity, and as plugins and themes grow, OOP helps manage that growth without descending into unmaintainable spaghetti.

The benefits are concrete. Encapsulation keeps related code together and hides complexity behind clean interfaces. Namespace isolation means you don't have to prefix every function with your plugin name to avoid collisions. Classes can be unit tested in isolation. Well-designed classes can be reused across projects. And perhaps most importantly, OOP code tends to be easier to understand and modify months later when you've forgotten how everything works.

The Pragmatic Approach

You don't need to go full enterprise architecture for a simple plugin. OOP is a tool, not a religion. Use classes where they add value; don't force patterns where procedural code works fine. A 50-line plugin doesn't need a dependency injection container.

Fundamentals

OOP Fundamentals for WordPress

Before diving into WordPress-specific patterns, let's establish the core OOP concepts you'll use constantly. If you're already comfortable with classes, inheritance, and interfaces, feel free to skip ahead.

Classes and Objects

A class is a blueprint; an object is an instance of that blueprint. Classes define properties (data) and methods (behavior) that belong together. When you instantiate a class with the new keyword, you get an object that has its own copy of the properties and can call the methods.

class My_Plugin {
    private string $version = '1.0.0';

    public function get_version(): string {
        return $this->version;
    }
}

$plugin = new My_Plugin();
echo $plugin->get_version(); // '1.0.0'

Visibility

PHP gives you three visibility levels for properties and methods. Public members are accessible from anywhere. Protected members are accessible from the class itself and any classes that extend it. Private members are accessible only within the class that defines them. I default to private and only increase visibility when there's a reason—it limits how code can depend on internal implementation details.

Inheritance

Child classes extend parent classes, inheriting their properties and methods while adding or overriding behavior. This is useful when you have multiple classes that share common functionality but differ in specifics.

class Base_Handler {
    protected function log( string $message ): void {
        error_log( $message );
    }
}

class Form_Handler extends Base_Handler {
    public function process(): void {
        $this->log( 'Processing form' );
        // Form processing logic
    }
}

Interfaces

Interfaces define contracts that classes must fulfill. They specify method signatures without implementations. Any class implementing an interface must provide those methods, which enables polymorphism—you can write code that works with any object fulfilling the interface, regardless of its concrete class.

interface Renderable {
    public function render(): string;
}

class Widget implements Renderable {
    public function render(): string {
        return '<div class="widget">Content</div>';
    }
}

PHP 8 Features

PHP 8 introduced constructor property promotion, which reduces boilerplate significantly. Instead of declaring properties and assigning them in the constructor, you can do both at once: public function __construct(private string $name) {}. This single line replaces three or four lines of traditional code.
Structure

Structuring a WordPress Plugin with OOP

A well-organized OOP plugin has clear structure that makes it obvious where different types of code belong. When you need to add a feature, you know where to put it. When you need to fix a bug, you know where to look.

Recommended Directory Structure

I've settled on a structure that works well for most plugins. The main plugin file stays minimal—just enough to bootstrap the plugin. Actual functionality lives in classes under a src directory, organized by domain or layer.

my-plugin/
├── my-plugin.php          # Bootstrap file
├── composer.json          # Autoloading config
├── src/
│   ├── Plugin.php         # Main plugin class
│   ├── Admin/
│   │   ├── Settings.php
│   │   └── Menu.php
│   ├── Frontend/
│   │   └── Shortcodes.php
│   ├── Core/
│   │   ├── Loader.php
│   │   └── I18n.php
│   └── Includes/
│       └── Helpers.php
└── tests/
    └── ...

The Bootstrap File

Keep the main plugin file minimal. Its job is to check dependencies, load the autoloader, and instantiate the main plugin class. Everything else belongs elsewhere.

<?php
/**
 * Plugin Name: My Plugin
 * Version: 1.0.0
 */

if ( ! defined( 'ABSPATH' ) ) {
    exit;
}

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

use MyPlugin\Plugin;

function my_plugin(): Plugin {
    static $instance = null;
    if ( null === $instance ) {
        $instance = new Plugin();
    }
    return $instance;
}

my_plugin()->run();

The Main Plugin Class

The main plugin class coordinates initialization, sets up hooks, and provides a central point for other code to access plugin functionality. It doesn't do much itself—it orchestrates other classes that do the actual work.

namespace MyPlugin;

class Plugin {
    private Loader $loader;

    public function __construct() {
        $this->loader = new Loader();
        $this->define_admin_hooks();
        $this->define_public_hooks();
    }

    private function define_admin_hooks(): void {
        $admin = new Admin\Settings();
        $this->loader->add_action( 'admin_menu', $admin, 'add_menu_page' );
        $this->loader->add_action( 'admin_init', $admin, 'register_settings' );
    }

    private function define_public_hooks(): void {
        $public = new Frontend\Shortcodes();
        $this->loader->add_shortcode( 'my_shortcode', $public, 'render' );
    }

    public function run(): void {
        $this->loader->run();
    }
}
Hooks

Working with WordPress Hooks

Hooks are the heart of WordPress extensibility, and they work perfectly with OOP. The key is understanding how to pass object methods as callbacks to add_action() and add_filter().

Instance Method Callbacks

When you want a method on a specific object to handle a hook, pass an array with the object and method name. The $this variable inside a class refers to the current object instance.

class My_Feature {
    public function __construct() {
        add_action( 'init', [ $this, 'initialize' ] );
        add_filter( 'the_content', [ $this, 'modify_content' ] );
    }

    public function initialize(): void {
        // Runs on init
    }

    public function modify_content( string $content ): string {
        return $content . '<p>Added by plugin</p>';
    }
}

Static Method Callbacks

For methods that don't need object state, static methods work well. Use __CLASS__ or the full class name instead of $this.

class Utilities {
    public static function register_hooks(): void {
        add_action( 'wp_footer', [ __CLASS__, 'render_footer' ] );
    }

    public static function render_footer(): void {
        echo '<!-- Plugin footer -->';
    }
}

The Loader Pattern

Many OOP plugins centralize hook registration in a Loader class. This provides a single place to see all hooks the plugin registers, makes it easier to manage priorities, and enables testing by separating hook registration from hook definition.

class Loader {
    private array $actions = [];
    private array $filters = [];

    public function add_action(
        string $hook,
        object $component,
        string $callback,
        int $priority = 10,
        int $args = 1
    ): void {
        $this->actions[] = compact( 'hook', 'component', 'callback', 'priority', 'args' );
    }

    public function run(): void {
        foreach ( $this->actions as $hook ) {
            add_action(
                $hook['hook'],
                [ $hook['component'], $hook['callback'] ],
                $hook['priority'],
                $hook['args']
            );
        }
        // Similar for filters
    }
}

Hook Timing

Be careful when registering hooks in constructors. If your class is instantiated too late (after the hook has fired), your callback won't run. Ensure objects are created at the appropriate time, typically on plugins_loaded or init depending on what hooks you need.
SOLID

SOLID Principles in WordPress

SOLID principles guide object-oriented design toward code that's flexible, maintainable, and testable. These principles aren't WordPress-specific, but they apply directly to WordPress development. Understanding them improves your design decisions even when you don't follow them perfectly.

Single Responsibility Principle

A class should have one reason to change. In practice, this means classes should do one thing well. A class that handles settings should only handle settings—not also render admin pages and process forms. When you find yourself adding "and" to describe what a class does, consider splitting it.

Open/Closed Principle

Classes should be open for extension but closed for modification. Design classes so new behavior can be added without changing existing code. Abstract base classes and interfaces enable this—define a contract, then add implementations without touching the original.

// Base notification class
abstract class Notification {
    abstract public function send( string $message ): bool;
}

// Extend without modifying base
class Email_Notification extends Notification {
    public function send( string $message ): bool {
        return wp_mail( $this->recipient, 'Notification', $message );
    }
}

class Slack_Notification extends Notification {
    public function send( string $message ): bool {
        // Slack API call
    }
}

Liskov Substitution Principle

Child classes should be substitutable for parents without breaking behavior. If a function expects a Notification object, any Notification subclass should work correctly. This enables polymorphism—write code against interfaces, and any implementation works.

Interface Segregation Principle

Many specific interfaces beat one large interface. If a class only needs to save data, it shouldn't have to implement a massive interface that also includes delete, archive, and export methods. Keep interfaces focused on specific capabilities.

Dependency Inversion Principle

Depend on abstractions, not concretions. Instead of instantiating dependencies directly, accept them as constructor parameters typed to interfaces. This decouples classes and makes them testable—you can pass mock implementations during testing.

interface Cache_Interface {
    public function get( string $key ): mixed;
    public function set( string $key, mixed $value ): bool;
}

class My_Service {
    public function __construct( private Cache_Interface $cache ) {}

    public function get_data(): array {
        return $this->cache->get( 'my_data' ) ?: [];
    }
}

Why SOLID Matters

SOLID principles lead to code that's easier to test, extend, and maintain. You don't need to follow them religiously, but understanding them improves your design decisions and helps you recognize code smells.

Pragmatic Application

Don't over-engineer simple plugins. A 200-line plugin doesn't need dependency injection containers and abstract factories. Apply SOLID where complexity warrants it—typically in larger, longer-lived projects where the investment pays off.

Autoloading

Autoloading with Composer

Stop writing require statements. Composer's autoloader loads classes automatically when you use them, based on namespace-to-directory mapping. This eliminates manual file includes and enforces consistent naming conventions.

composer.json Configuration

PSR-4 autoloading maps namespace prefixes to directories. When you use a class, Composer knows which file to load based on the namespace.

{
    "name": "your-name/my-plugin",
    "autoload": {
        "psr-4": {
            "MyPlugin\\": "src/"
        }
    }
}

How PSR-4 Works

The namespace directly maps to the directory structure. MyPlugin\Admin\Settings lives in src/Admin/Settings.php. MyPlugin\Frontend\Widget lives in src/Frontend/Widget.php. This makes it obvious where any class lives just from its namespace.

Generate and Use the Autoloader

Run composer dump-autoload to generate the autoloader, then require it once in your main plugin file. After that, just use your classes—Composer handles the rest.

// In main plugin file
require_once __DIR__ . '/vendor/autoload.php';

// Now just use your classes
use MyPlugin\Admin\Settings;
$settings = new Settings();

Autoloading Benefits

Beyond convenience, autoloading improves performance by loading classes only when needed. It also enforces consistent naming conventions since filenames must match class names. And it eliminates an entire category of bugs from incorrect require paths.

Patterns

Common Patterns

Certain design patterns appear frequently in WordPress OOP development. Learning to recognize and apply them gives you proven solutions for common problems.

Singleton (Use Sparingly)

Singletons ensure only one instance of a class exists. They're common for main plugin classes but should be used carefully—they create global state and hidden dependencies that make testing harder.

class Plugin {
    private static ?Plugin $instance = null;

    public static function get_instance(): Plugin {
        if ( null === self::$instance ) {
            self::$instance = new self();
        }
        return self::$instance;
    }

    private function __construct() {
        // Prevent direct instantiation
    }
}

Factory

Factories create objects based on parameters, centralizing creation logic. When you need different implementations based on runtime conditions, a factory handles the decision-making.

class Notification_Factory {
    public static function create( string $type ): Notification {
        return match( $type ) {
            'email' => new Email_Notification(),
            'slack' => new Slack_Notification(),
            'sms'   => new SMS_Notification(),
            default => throw new InvalidArgumentException( "Unknown type: $type" ),
        };
    }
}

Repository

Repositories abstract data access, providing a clean API for querying and persisting data. They hide WordPress's query functions behind domain-focused methods.

class Post_Repository {
    public function find( int $id ): ?WP_Post {
        return get_post( $id );
    }

    public function find_by_type( string $type, int $limit = 10 ): array {
        return get_posts( [
            'post_type'      => $type,
            'posts_per_page' => $limit,
        ] );
    }

    public function save( array $data ): int|WP_Error {
        return wp_insert_post( $data );
    }
}

Singleton Warning

Singletons are often overused. They create hidden dependencies and make testing harder because you can't easily substitute mock implementations. Consider dependency injection instead—pass dependencies explicitly rather than reaching for global instances.
Testing

Testing OOP WordPress Code

One of OOP's biggest advantages is testability. Well-designed classes with clear interfaces and injected dependencies can be tested in isolation. You verify that each class works correctly without running the entire WordPress environment.

Unit Testing a Class

Simple classes with no external dependencies are easy to test. Instantiate the class, call methods, and verify the results match expectations.

class Calculator {
    public function add( int $a, int $b ): int {
        return $a + $b;
    }
}

// Test
class CalculatorTest extends WP_UnitTestCase {
    public function test_add(): void {
        $calc = new Calculator();
        $this->assertEquals( 5, $calc->add( 2, 3 ) );
    }
}

Mocking Dependencies

When classes depend on other objects, mock those dependencies to test classes in isolation. Mocks verify that your class interacts correctly with its dependencies without needing the real implementations.

class Order_Service {
    public function __construct( private Payment_Gateway $gateway ) {}

    public function process( Order $order ): bool {
        return $this->gateway->charge( $order->total );
    }
}

// Test with mock
class OrderServiceTest extends WP_UnitTestCase {
    public function test_process_calls_gateway(): void {
        $mock_gateway = $this->createMock( Payment_Gateway::class );
        $mock_gateway->expects( $this->once() )
            ->method( 'charge' )
            ->willReturn( true );

        $service = new Order_Service( $mock_gateway );
        $this->assertTrue( $service->process( new Order( 100 ) ) );
    }
}

Testing catches bugs before deployment, lets you refactor with confidence, documents expected behavior, and enables continuous integration. The upfront investment in testable architecture pays dividends throughout a project's life.

Conclusion

Getting Started

You don't need to become an OOP expert overnight. Start incrementally and evolve your approach as your understanding deepens and your projects demand more structure.

  1. Wrap your plugin in a class

    Even a simple main class that initializes your plugin is a good start. It removes functions from the global namespace and provides a foundation to build on.

  2. Group related functions

    Functions that work together belong in a class together. Admin settings, frontend rendering, API handling—each becomes a class with focused responsibility.

  3. Add namespaces and autoloading

    Set up Composer autoloading for cleaner includes and consistent structure. This is a one-time investment that pays off immediately.

  4. Apply SOLID principles gradually

    As code grows, refactor toward better separation of concerns. Extract interfaces, inject dependencies, and keep classes focused.

  5. Write tests

    Testing reinforces good OOP design and catches regressions. Start with simple unit tests and expand coverage over time.

Object-oriented programming isn't about following rules—it's about managing complexity. As your WordPress projects grow, OOP provides the structure to keep code understandable and maintainable. Start simple, evolve as needed, and let the principles guide you toward better code.

Frequently Asked Questions

Does WordPress support object-oriented programming?

Yes. While WordPress core uses primarily procedural code with some OOP, you can write fully object-oriented plugins and themes. WordPress's hooks system works perfectly with OOP approaches, and many modern plugins are entirely class-based.

Should I rewrite my existing procedural plugin as OOP?

Not necessarily. If your plugin works and is maintainable, a rewrite may not be worth the effort. Consider OOP for new features or when procedural code becomes difficult to manage. Gradual refactoring is often more practical than complete rewrites.

What's the minimum PHP version for modern OOP in WordPress?

PHP 7.4+ gives you typed properties, arrow functions, and null coalescing assignment. PHP 8.0+ adds named arguments, attributes, and constructor promotion. WordPress 6.x requires PHP 7.4 minimum, making modern OOP features safe to use.

How do I handle WordPress hooks in an OOP plugin?

Use class methods as callbacks: add_action('init', [$this, 'method_name']). For static methods: add_action('init', [__CLASS__, 'static_method']). Many developers create a dedicated Hooks or Loader class to register all hooks in one place.
PHP WordPress Object-Oriented Programming Software Architecture Development Best Practices
William Alexander

William Alexander

Senior Web Developer

25+ years of web development experience spanning higher education and small business. Currently Senior Web Developer at Wake Forest University.

Related Articles

WordPress Enterprise

WordPress Plugin Development Fundamentals

13 min read
WordPress Enterprise

Composer for WordPress: Package Management Done Right

13 min read

Need help modernizing your WordPress codebase?

I help teams adopt modern PHP practices in WordPress development. Let's discuss how to improve your plugin or theme architecture.

© 2026 williamalexander.co. All rights reserved.