Enterprise WordPress Architecture: OOP, Composer & Modern Build Pipelines
Moving beyond procedural code is the hallmark of a senior WordPress engineer. This architectural guide covers the implementation of Object-Oriented Design (SOLID, MVC) and the integration of industry-standard tooling—Composer for PHP dependencies and npm for modern asset compilation—to create scalable, maintainable software.
OOP Basics for WordPress
Classes in Plugins
Classes encapsulate plugin functionality into reusable, organized units that prevent global namespace pollution and improve maintainability—the foundation of professional WordPress development.
class My_Plugin { private $version = '1.0.0'; private $plugin_name = 'my-plugin'; public function run() { add_action('init', [$this, 'init_plugin']); add_action('admin_menu', [$this, 'add_admin_menu']); } public function init_plugin() { // Plugin initialization logic } } // Usage in main plugin file $plugin = new My_Plugin(); $plugin->run();
Static Methods
Static methods belong to the class itself rather than instances, useful for utility functions and accessing plugin data without instantiation—called using ClassName::method().
class Plugin_Utils { private static $instance = null; public static function get_plugin_path() { return plugin_dir_path(__FILE__); } public static function get_plugin_url() { return plugin_dir_url(__FILE__); } public static function sanitize_key($key) { return preg_replace('/[^a-z0-9_\-]/', '', strtolower($key)); } } // Usage - no instantiation needed $path = Plugin_Utils::get_plugin_path(); $url = Plugin_Utils::get_plugin_url();
Instance Methods
Instance methods operate on specific object instances and can access instance properties via $this, providing encapsulated behavior unique to each object.
class Post_Handler { private $post_type; private $meta_keys = []; public function set_post_type($type) { $this->post_type = $type; return $this; // Fluent interface } public function add_meta_key($key) { $this->meta_keys[] = $key; return $this; } public function register() { register_post_type($this->post_type, [ 'public' => true, 'label' => ucfirst($this->post_type) ]); } } // Each instance is independent $books = new Post_Handler(); $books->set_post_type('book')->add_meta_key('isbn')->register(); $movies = new Post_Handler(); $movies->set_post_type('movie')->add_meta_key('director')->register();
Constructors
The __construct() method automatically executes when an object is created, ideal for setting up hooks, initializing properties, and injecting dependencies.
class My_Plugin { private $loader; private $admin; private $version; public function __construct($version = '1.0.0') { $this->version = $version; $this->load_dependencies(); $this->define_admin_hooks(); $this->define_public_hooks(); } private function load_dependencies() { require_once plugin_dir_path(__FILE__) . 'class-loader.php'; $this->loader = new Plugin_Loader(); } private function define_admin_hooks() { $this->loader->add_action('admin_menu', $this, 'add_menu'); } } // Constructor runs automatically $plugin = new My_Plugin('2.0.0');
Singleton Pattern
Singleton ensures only one instance of a class exists throughout the request lifecycle—essential for main plugin classes to prevent duplicate hook registrations and memory waste.
class My_Plugin_Singleton { private static $instance = null; private $settings = []; // Private constructor prevents direct instantiation private function __construct() { $this->settings = get_option('my_plugin_settings', []); } // Private clone prevents duplication private function __clone() {} // Private wakeup prevents unserialization private function __wakeup() {} public static function get_instance() { if (null === self::$instance) { self::$instance = new self(); } return self::$instance; } public function get_setting($key) { return $this->settings[$key] ?? null; } } // Always returns the same instance $plugin = My_Plugin_Singleton::get_instance();
┌─────────────────────────────────────┐
│ Singleton Pattern │
├─────────────────────────────────────┤
│ Request 1 ──┐ │
│ Request 2 ──┼──► get_instance() ───┤
│ Request 3 ──┘ │ │
│ ▼ │
│ ┌─────────────────┐ │
│ │ Single Instance │ │
│ │ (created once)│ │
│ └─────────────────┘ │
└─────────────────────────────────────┘
Factory Pattern
Factory pattern creates objects without exposing instantiation logic, allowing flexible object creation based on conditions—useful for creating different handler types dynamically.
class Handler_Factory { public static function create($type, $args = []) { switch ($type) { case 'post': return new Post_Handler($args); case 'user': return new User_Handler($args); case 'comment': return new Comment_Handler($args); default: throw new InvalidArgumentException("Unknown handler: {$type}"); } } } // Usage - clean object creation $post_handler = Handler_Factory::create('post', ['post_type' => 'book']); $user_handler = Handler_Factory::create('user', ['role' => 'editor']); // Dynamic creation from config $handlers = ['post', 'user', 'comment']; foreach ($handlers as $type) { $handler = Handler_Factory::create($type); $handler->init(); }
Plugin Architecture
Main Plugin Class
The main plugin class serves as the entry point and orchestrator, initializing components, defining hooks, and coordinating between admin, public, and loader classes.
// my-plugin.php (main file) namespace MyPlugin; final class Plugin { const VERSION = '1.0.0'; private static $instance = null; private $loader; private $admin; private $public; public static function instance() { if (null === self::$instance) { self::$instance = new self(); } return self::$instance; } private function __construct() { $this->define_constants(); $this->load_dependencies(); $this->init_hooks(); } private function define_constants() { define('MYPLUGIN_PATH', plugin_dir_path(__FILE__)); define('MYPLUGIN_URL', plugin_dir_url(__FILE__)); } private function load_dependencies() { $this->loader = new Loader(); $this->admin = new Admin\Admin_Controller(); $this->public = new Frontend\Public_Controller(); } public function run() { $this->loader->run(); } } // Bootstrap function my_plugin() { return Plugin::instance(); } my_plugin()->run();
Loader Class
The Loader class manages all WordPress hooks centrally, storing actions and filters in arrays and registering them at runtime—providing a clean separation between hook declaration and execution.
class Loader { protected $actions = []; protected $filters = []; public function add_action($hook, $component, $callback, $priority = 10, $args = 1) { $this->actions[] = [ 'hook' => $hook, 'component' => $component, 'callback' => $callback, 'priority' => $priority, 'args' => $args ]; return $this; } public function add_filter($hook, $component, $callback, $priority = 10, $args = 1) { $this->filters[] = compact('hook', 'component', 'callback', 'priority', 'args'); return $this; } public function run() { foreach ($this->filters as $hook) { add_filter( $hook['hook'], [$hook['component'], $hook['callback']], $hook['priority'], $hook['args'] ); } foreach ($this->actions as $hook) { add_action( $hook['hook'], [$hook['component'], $hook['callback']], $hook['priority'], $hook['args'] ); } } }
Admin Class
The Admin class handles all WordPress dashboard functionality including admin menus, settings pages, admin scripts/styles, and meta boxes—isolated from public-facing code.
namespace MyPlugin\Admin; class Admin_Controller { private $plugin_name; private $version; public function __construct($plugin_name, $version) { $this->plugin_name = $plugin_name; $this->version = $version; } public function enqueue_styles() { wp_enqueue_style( $this->plugin_name . '-admin', MYPLUGIN_URL . 'assets/css/admin.css', [], $this->version ); } public function enqueue_scripts() { wp_enqueue_script( $this->plugin_name . '-admin', MYPLUGIN_URL . 'assets/js/admin.js', ['jquery'], $this->version, true ); } public function add_menu_pages() { add_menu_page( 'My Plugin Settings', 'My Plugin', 'manage_options', $this->plugin_name, [$this, 'render_settings_page'], 'dashicons-admin-generic' ); } public function render_settings_page() { include MYPLUGIN_PATH . 'admin/views/settings-page.php'; } }
Public Class
The Public class manages frontend functionality including public scripts/styles, shortcodes, and template modifications—completely separated from admin-specific code.
namespace MyPlugin\Frontend; class Public_Controller { private $plugin_name; private $version; public function __construct($plugin_name, $version) { $this->plugin_name = $plugin_name; $this->version = $version; } public function enqueue_styles() { wp_enqueue_style( $this->plugin_name . '-public', MYPLUGIN_URL . 'assets/css/public.css', [], $this->version ); } public function enqueue_scripts() { wp_enqueue_script( $this->plugin_name . '-public', MYPLUGIN_URL . 'assets/js/public.js', ['jquery'], $this->version, true ); wp_localize_script($this->plugin_name . '-public', 'myPluginAjax', [ 'ajaxurl' => admin_url('admin-ajax.php'), 'nonce' => wp_create_nonce('my_plugin_nonce') ]); } public function register_shortcodes() { add_shortcode('my_plugin', [$this, 'render_shortcode']); } }
Includes Organization
The includes directory contains core plugin classes, utilities, and shared functionality—structured logically for easy navigation and maintenance.
my-plugin/
├── includes/
│ ├── class-plugin.php # Main plugin class
│ ├── class-loader.php # Hook loader
│ ├── class-activator.php # Activation logic
│ ├── class-deactivator.php # Deactivation logic
│ ├── class-i18n.php # Internationalization
│ │
│ ├── abstracts/
│ │ ├── abstract-controller.php
│ │ └── abstract-model.php
│ │
│ ├── interfaces/
│ │ ├── interface-renderable.php
│ │ └── interface-hookable.php
│ │
│ ├── traits/
│ │ ├── trait-singleton.php
│ │ └── trait-ajax-handler.php
│ │
│ └── models/
│ ├── class-settings.php
│ └── class-post-type.php
Assets Organization
Assets are organized by type and context (admin vs public), with source files separated from compiled/minified versions for efficient development workflow.
my-plugin/
├── assets/
│ ├── css/
│ │ ├── admin.css
│ │ ├── admin.min.css
│ │ ├── public.css
│ │ └── public.min.css
│ │
│ ├── js/
│ │ ├── admin.js
│ │ ├── admin.min.js
│ │ ├── public.js
│ │ └── public.min.js
│ │
│ ├── images/
│ │ ├── icons/
│ │ └── logos/
│ │
│ └── src/ # Source files (if using build tools)
│ ├── scss/
│ │ ├── admin/
│ │ └── public/
│ └── ts/ # TypeScript source
│ ├── admin/
│ └── public/
Templates Organization
Templates are separated by context with a clear hierarchy, supporting theme overriding for maximum flexibility—following WordPress template hierarchy patterns.
my-plugin/
├── templates/
│ ├── admin/
│ │ ├── settings-page.php
│ │ ├── meta-boxes/
│ │ │ └── post-meta.php
│ │ └── partials/
│ │ ├── header.php
│ │ └── footer.php
│ │
│ ├── public/
│ │ ├── single-custom-post.php
│ │ ├── archive-custom-post.php
│ │ └── shortcodes/
│ │ └── gallery.php
│ │
│ └── emails/
│ ├── notification.php
│ └── welcome.php
// Template loader with theme override support
class Template_Loader {
public function get_template($template_name, $args = []) {
// Check theme first
$located = locate_template('my-plugin/' . $template_name);
// Fallback to plugin
if (!$located) {
$located = MYPLUGIN_PATH . 'templates/' . $template_name;
}
if ($args) extract($args);
include $located;
}
}
Namespaces
Namespace Declaration
Namespaces organize code into logical groups, preventing naming conflicts with WordPress core, other plugins, and themes—declared at the top of each PHP file.
<?php // File: includes/class-post-handler.php namespace MyPlugin\Handlers; class Post_Handler { public function create_post($data) { return wp_insert_post($data); } } // File: includes/models/class-post-model.php namespace MyPlugin\Models; class Post_Handler { // Same class name, different namespace - NO CONFLICT! private $post_id; public function __construct($id) { $this->post_id = $id; } }
┌────────────────────────────────────────┐
│ Namespace Hierarchy │
├────────────────────────────────────────┤
│ MyPlugin (root) │
│ ├── Admin │
│ │ ├── Admin_Controller │
│ │ └── Settings_Page │
│ ├── Frontend │
│ │ ├── Public_Controller │
│ │ └── Shortcodes │
│ ├── Models │
│ │ ├── Post_Model │
│ │ └── User_Model │
│ └── Core │
│ ├── Loader │
│ └── Plugin │
└────────────────────────────────────────┘
Use Statements
The use statement imports classes from other namespaces, eliminating the need for fully qualified class names and improving code readability.
<?php namespace MyPlugin\Admin; // Import specific classes use MyPlugin\Core\Loader; use MyPlugin\Models\Settings; use MyPlugin\Interfaces\Renderable; // Import with alias (when names conflict) use MyPlugin\Admin\Post_Handler as AdminPostHandler; use MyPlugin\Frontend\Post_Handler as PublicPostHandler; // Import functions and constants (PHP 5.6+) use function MyPlugin\Helpers\sanitize_input; use const MyPlugin\Config\VERSION; class Admin_Controller implements Renderable { private $loader; private $settings; public function __construct() { $this->loader = new Loader(); // No need for \MyPlugin\Core\Loader $this->settings = new Settings(); // Clean and readable } public function render() { $admin_handler = new AdminPostHandler(); $public_handler = new PublicPostHandler(); } }
Aliasing
Aliasing assigns alternative names to imported classes using as, resolving conflicts when multiple namespaces have identically named classes.
<?php namespace MyPlugin\Controllers; // Aliasing to avoid conflicts use MyPlugin\Models\User as UserModel; use MyPlugin\Entities\User as UserEntity; use MyPlugin\Repositories\User as UserRepository; // Aliasing for convenience use MyPlugin\Services\Authentication\Manager as AuthManager; use MyPlugin\Database\QueryBuilder as DB; class User_Controller { private $user_model; private $user_repo; private $auth; public function __construct() { $this->user_model = new UserModel(); // MyPlugin\Models\User $this->user_repo = new UserRepository(); // MyPlugin\Repositories\User $this->auth = new AuthManager(); // Short alias for long class name } public function get_user($id) { return DB::table('users') // Clean, readable queries ->where('id', $id) ->first(); } }
Namespace Conventions
WordPress namespace conventions follow vendor prefix patterns, using PascalCase for namespace segments and underscores for class names to maintain WordPress coding standards.
<?php // ✅ GOOD: Clear vendor prefix and hierarchy namespace MyCompany\MyPlugin\Admin\Settings; namespace MyPlugin\Core\Database; namespace MyPlugin\Api\V1\Endpoints; // ❌ BAD: Too generic, conflicts likely namespace Admin; namespace Plugin; namespace Utils; // Convention structure // {VendorName}\{PluginName}\{Module}\{Submodule} // WordPress-compatible naming namespace MyPlugin\PostTypes; class Custom_Post_Type { // Underscores (WP style) // ... } // PSR-4 compatible naming namespace MyPlugin\PostTypes; class CustomPostType { // PascalCase (PSR style) // ... } // Recommended file structure matching namespace // src/PostTypes/CustomPostType.php → MyPlugin\PostTypes\CustomPostType // src/Admin/Settings/General.php → MyPlugin\Admin\Settings\General
PSR-4 Standard
PSR-4 defines an autoloading standard mapping fully qualified class names to file paths, enabling automatic class loading without manual requires—the modern PHP standard.
// composer.json { "name": "mycompany/my-plugin", "autoload": { "psr-4": { "MyPlugin\\": "src/" } } } // Directory structure (PSR-4 compliant) my-plugin/ ├── src/ │ ├── Core/ │ │ ├── Plugin.php → MyPlugin\Core\Plugin │ │ └── Loader.php → MyPlugin\Core\Loader │ ├── Admin/ │ │ ├── Controller.php → MyPlugin\Admin\Controller │ │ └── Settings/ │ │ └── General.php → MyPlugin\Admin\Settings\General │ └── Models/ │ └── Post.php → MyPlugin\Models\Post // Class file example: src/Admin/Settings/General.php <?php namespace MyPlugin\Admin\Settings; class General { // File path MUST match namespace structure }
┌─────────────────────────────────────────────────────────┐
│ PSR-4 Mapping │
├─────────────────────────────────────────────────────────┤
│ Namespace Prefix │ Base Directory │
├──────────────────────┼──────────────────────────────────┤
│ MyPlugin\ │ /src/ │
├──────────────────────┴──────────────────────────────────┤
│ │
│ MyPlugin\Core\Plugin │
│ └─────┬────┘ │
│ ▼ │
│ /src/ + Core/Plugin + .php = /src/Core/Plugin.php │
│ │
└─────────────────────────────────────────────────────────┘
Autoloading
spl_autoload_register()
PHP's native autoloading function registers a callback that's invoked when an undefined class is used, enabling automatic file inclusion without manual require statements.
// Manual autoloader without Composer spl_autoload_register(function ($class) { // Only handle our plugin's namespace $prefix = 'MyPlugin\\'; if (strpos($class, $prefix) !== 0) { return; // Not our class, let other autoloaders handle it } // Remove namespace prefix $relative_class = substr($class, strlen($prefix)); // Convert namespace to file path // MyPlugin\Admin\Controller → admin/class-controller.php $file = plugin_dir_path(__FILE__) . 'includes/'; $file .= str_replace('\\', '/', strtolower($relative_class)); $file .= '.php'; if (file_exists($file)) { require_once $file; } }); // Now classes load automatically $admin = new MyPlugin\Admin\Controller(); // Auto-loads the file $model = new MyPlugin\Models\Post(); // Auto-loads the file
PSR-4 Autoloading
PSR-4 autoloading maps namespace prefixes to directory paths, with the remaining namespace segments directly corresponding to subdirectories—the standard for modern PHP projects.
// Custom PSR-4 autoloader implementation class PSR4_Autoloader { private $prefixes = []; public function register() { spl_autoload_register([$this, 'load_class']); } public function add_namespace($prefix, $base_dir) { $prefix = trim($prefix, '\\') . '\\'; $this->prefixes[$prefix] = rtrim($base_dir, '/') . '/'; } public function load_class($class) { foreach ($this->prefixes as $prefix => $base_dir) { if (strpos($class, $prefix) === 0) { $relative = substr($class, strlen($prefix)); $file = $base_dir . str_replace('\\', '/', $relative) . '.php'; if (file_exists($file)) { require $file; return true; } } } return false; } } // Usage $loader = new PSR4_Autoloader(); $loader->add_namespace('MyPlugin', __DIR__ . '/src'); $loader->add_namespace('MyPlugin\\Tests', __DIR__ . '/tests'); $loader->register();
Composer Autoloader
Composer generates an optimized autoloader from composer.json configuration, handling PSR-4, PSR-0, classmap, and files autoloading—the industry standard for PHP dependency management.
// composer.json { "name": "mycompany/my-plugin", "autoload": { "psr-4": { "MyPlugin\\": "src/" }, "classmap": [ "includes/legacy/" ], "files": [ "includes/helpers.php" ] }, "autoload-dev": { "psr-4": { "MyPlugin\\Tests\\": "tests/" } } }
// Main plugin file <?php /** * Plugin Name: My Plugin */ // Load Composer autoloader if (file_exists(__DIR__ . '/vendor/autoload.php')) { require_once __DIR__ . '/vendor/autoload.php'; } // Now all classes are available automatically use MyPlugin\Core\Plugin; use MyPlugin\Admin\Controller; Plugin::instance()->run();
# Generate autoloader composer dump-autoload # Optimized for production composer dump-autoload --optimize --classmap-authoritative
Classmap Autoloading
Classmap autoloading scans directories and builds a static array mapping class names to file paths—faster than PSR-4 for production since no path calculations are needed at runtime.
// Generated classmap (vendor/composer/autoload_classmap.php) return array( 'MyPlugin\\Core\\Plugin' => $baseDir . '/src/Core/Plugin.php', 'MyPlugin\\Admin\\Controller' => $baseDir . '/src/Admin/Controller.php', 'MyPlugin\\Models\\Post' => $baseDir . '/src/Models/Post.php', // All classes pre-mapped );
// composer.json - using classmap for legacy code { "autoload": { "psr-4": { "MyPlugin\\": "src/" }, "classmap": [ "includes/legacy-classes/", "includes/third-party/" ] } }
// Manual classmap implementation class Classmap_Autoloader { private static $classmap = [ 'Legacy_Class' => '/includes/legacy/class-legacy.php', 'Old_Widget' => '/includes/widgets/old-widget.php', ]; public static function load($class) { if (isset(self::$classmap[$class])) { require_once MYPLUGIN_PATH . self::$classmap[$class]; } } } spl_autoload_register(['Classmap_Autoloader', 'load']);
Dependency Injection
Constructor Injection
Constructor injection passes dependencies as constructor parameters, making required dependencies explicit and enabling easier testing with mocks—the most common and recommended DI method.
// Without DI (tight coupling) ❌ class Post_Service { private $repository; public function __construct() { $this->repository = new Post_Repository(); // Hard-coded dependency } } // With Constructor Injection ✅ class Post_Service { private $repository; private $cache; private $logger; public function __construct( Post_Repository_Interface $repository, Cache_Interface $cache, Logger_Interface $logger ) { $this->repository = $repository; $this->cache = $cache; $this->logger = $logger; } public function get_post($id) { if ($cached = $this->cache->get("post_{$id}")) { return $cached; } $post = $this->repository->find($id); $this->logger->info("Post {$id} retrieved"); return $post; } } // Usage - dependencies are explicit and swappable $service = new Post_Service( new WP_Post_Repository(), new Redis_Cache(), new File_Logger() );
Setter Injection
Setter injection uses methods to inject optional dependencies after object creation, useful for optional services or when circular dependencies exist—less strict than constructor injection.
class Email_Service { private $mailer; private $template_engine; private $logger; // Required dependency via constructor public function __construct(Mailer_Interface $mailer) { $this->mailer = $mailer; } // Optional dependency via setter public function set_template_engine(Template_Interface $engine) { $this->template_engine = $engine; return $this; // Fluent interface } // Optional dependency via setter public function set_logger(Logger_Interface $logger) { $this->logger = $logger; return $this; } public function send($to, $subject, $body) { if ($this->logger) { $this->logger->info("Sending email to {$to}"); } if ($this->template_engine) { $body = $this->template_engine->render($body); } return $this->mailer->send($to, $subject, $body); } } // Fluent configuration $emailer = (new Email_Service(new SMTP_Mailer())) ->set_template_engine(new Twig_Engine()) ->set_logger(new WP_Logger());
Container Patterns
A dependency injection container (DIC) manages object creation and dependencies automatically, resolving and injecting dependencies based on configuration or type hints.
class Simple_Container { private $bindings = []; private $instances = []; public function bind($abstract, $concrete) { $this->bindings[$abstract] = $concrete; } public function singleton($abstract, $concrete) { $this->bind($abstract, function($container) use ($concrete, $abstract) { if (!isset($this->instances[$abstract])) { $this->instances[$abstract] = $concrete($container); } return $this->instances[$abstract]; }); } public function make($abstract) { if (isset($this->bindings[$abstract])) { $concrete = $this->bindings[$abstract]; return is_callable($concrete) ? $concrete($this) : new $concrete(); } return new $abstract(); } } // Usage $container = new Simple_Container(); $container->bind(Logger_Interface::class, File_Logger::class); $container->bind(Cache_Interface::class, Redis_Cache::class); $container->singleton(Database::class, function($c) { return new Database(DB_HOST, DB_NAME, DB_USER, DB_PASS); }); $container->bind(Post_Service::class, function($c) { return new Post_Service( $c->make(Post_Repository::class), $c->make(Cache_Interface::class), $c->make(Logger_Interface::class) ); }); $service = $container->make(Post_Service::class);
Service Containers
A service container extends basic DI containers with automatic dependency resolution, service providers, and lifecycle management—the backbone of modern PHP frameworks.
class Service_Container { private static $instance; private $services = []; private $resolved = []; public static function getInstance() { return self::$instance ??= new self(); } public function register($name, callable $resolver, $shared = false) { $this->services[$name] = [ 'resolver' => $resolver, 'shared' => $shared ]; } public function get($name) { if (!isset($this->services[$name])) { throw new Exception("Service {$name} not registered"); } $service = $this->services[$name]; if ($service['shared'] && isset($this->resolved[$name])) { return $this->resolved[$name]; } $resolved = $service['resolver']($this); if ($service['shared']) { $this->resolved[$name] = $resolved; } return $resolved; } } // Service Provider pattern class Plugin_Service_Provider { public function register(Service_Container $container) { $container->register('database', fn($c) => new Database(), true); $container->register('cache', fn($c) => new WP_Cache(), true); $container->register('logger', fn($c) => new File_Logger(MYPLUGIN_PATH . 'logs/')); $container->register('post.repository', function($c) { return new Post_Repository($c->get('database')); }); $container->register('post.service', function($c) { return new Post_Service( $c->get('post.repository'), $c->get('cache') ); }, true); } }
Design Patterns
Singleton
Singleton restricts class instantiation to a single object, accessed via a static method—use sparingly in WordPress for main plugin classes and global services only.
trait Singleton_Trait { private static $instance = null; public static function instance() { if (null === self::$instance) { self::$instance = new static(); } return self::$instance; } private function __construct() { $this->init(); } private function __clone() {} public function __wakeup() { throw new Exception("Cannot unserialize singleton"); } abstract protected function init(); } class Plugin_Core { use Singleton_Trait; protected function init() { add_action('init', [$this, 'register_post_types']); } public function register_post_types() { // Registration logic } } // Usage - always same instance Plugin_Core::instance()->register_post_types();
Factory
Factory pattern encapsulates object creation logic, providing a clean interface to instantiate different implementations based on conditions—ideal for creating handlers, validators, or formatters.
interface Payment_Gateway { public function process($amount); } class Stripe_Gateway implements Payment_Gateway { public function process($amount) { /* Stripe logic */ } } class PayPal_Gateway implements Payment_Gateway { public function process($amount) { /* PayPal logic */ } } class Payment_Gateway_Factory { private $gateways = []; public function register($name, $class) { $this->gateways[$name] = $class; } public function create($name, array $config = []) { if (!isset($this->gateways[$name])) { throw new InvalidArgumentException("Unknown gateway: {$name}"); } $class = $this->gateways[$name]; return new $class($config); } } // Usage $factory = new Payment_Gateway_Factory(); $factory->register('stripe', Stripe_Gateway::class); $factory->register('paypal', PayPal_Gateway::class); $gateway = $factory->create(get_option('payment_gateway', 'stripe')); $gateway->process(99.99);
Observer
Observer pattern allows objects to subscribe to events and react when those events occur—WordPress hooks (actions/filters) are a built-in implementation of this pattern.
interface Observer { public function update($event, $data); } class Event_Dispatcher { private $observers = []; public function attach($event, Observer $observer) { $this->observers[$event][] = $observer; } public function detach($event, Observer $observer) { $key = array_search($observer, $this->observers[$event] ?? [], true); if ($key !== false) { unset($this->observers[$event][$key]); } } public function dispatch($event, $data = null) { foreach ($this->observers[$event] ?? [] as $observer) { $observer->update($event, $data); } } } class Email_Observer implements Observer { public function update($event, $data) { if ($event === 'order.completed') { wp_mail($data['email'], 'Order Confirmation', 'Your order is complete!'); } } } // Usage $dispatcher = new Event_Dispatcher(); $dispatcher->attach('order.completed', new Email_Observer()); $dispatcher->attach('order.completed', new Inventory_Observer()); $dispatcher->dispatch('order.completed', ['order_id' => 123, 'email' => 'user@example.com']);
Strategy
Strategy pattern defines a family of interchangeable algorithms, allowing the algorithm to vary independently from clients that use it—perfect for different pricing rules, validators, or formatters.
interface Pricing_Strategy { public function calculate($base_price); } class Regular_Pricing implements Pricing_Strategy { public function calculate($base_price) { return $base_price; } } class Member_Pricing implements Pricing_Strategy { public function calculate($base_price) { return $base_price * 0.9; // 10% discount } } class VIP_Pricing implements Pricing_Strategy { public function calculate($base_price) { return $base_price * 0.75; // 25% discount } } class Price_Calculator { private $strategy; public function set_strategy(Pricing_Strategy $strategy) { $this->strategy = $strategy; } public function get_price($base_price) { return $this->strategy->calculate($base_price); } } // Usage - strategy selected at runtime $calculator = new Price_Calculator(); $user_type = get_user_meta(get_current_user_id(), 'membership_type', true); $strategies = [ 'vip' => new VIP_Pricing(), 'member' => new Member_Pricing(), 'regular' => new Regular_Pricing() ]; $calculator->set_strategy($strategies[$user_type] ?? $strategies['regular']); echo $calculator->get_price(100); // Price varies by user type
Repository
Repository pattern abstracts data access logic, providing a collection-like interface to domain objects—decoupling business logic from WordPress database functions.
interface Post_Repository_Interface { public function find($id); public function findAll(array $criteria = []); public function save($entity); public function delete($id); } class WP_Post_Repository implements Post_Repository_Interface { private $post_type; public function __construct($post_type = 'post') { $this->post_type = $post_type; } public function find($id) { $post = get_post($id); return $post ? $this->to_entity($post) : null; } public function findAll(array $criteria = []) { $args = array_merge([ 'post_type' => $this->post_type, 'posts_per_page' => -1, 'post_status' => 'publish' ], $criteria); return array_map([$this, 'to_entity'], get_posts($args)); } public function save($entity) { $data = [ 'post_title' => $entity->title, 'post_content' => $entity->content, 'post_type' => $this->post_type, 'post_status' => 'publish' ]; if ($entity->id) { $data['ID'] = $entity->id; return wp_update_post($data); } return wp_insert_post($data); } private function to_entity($post) { return new Post_Entity($post->ID, $post->post_title, $post->post_content); } }
MVC Architecture
MVC separates application logic into Models (data), Views (presentation), and Controllers (request handling)—organizing plugin code for maintainability and testability.
┌─────────────────────────────────────────────────────────┐
│ MVC Flow │
├─────────────────────────────────────────────────────────┤
│ │
│ User Request │
│ │ │
│ ▼ │
│ ┌──────────────┐ ┌─────────────┐ │
│ │ Controller │──────│ Model │ │
│ │ (handles req)│ │(data/logic) │ │
│ └──────────────┘ └─────────────┘ │
│ │ │ │
│ │ │ data │
│ ▼ ▼ │
│ ┌──────────────────────────────────┐ │
│ │ View │ │
│ │ (renders output) │ │
│ └──────────────────────────────────┘ │
│ │ │
│ ▼ │
│ Response to User │
└─────────────────────────────────────────────────────────┘
// Controller class Book_Controller { private $model; private $view; public function __construct(Book_Model $model, View_Renderer $view) { $this->model = $model; $this->view = $view; } public function index() { $books = $this->model->get_all(); return $this->view->render('books/index', ['books' => $books]); } public function show($id) { $book = $this->model->find($id); return $this->view->render('books/show', ['book' => $book]); } } // Model class Book_Model { public function get_all() { return get_posts(['post_type' => 'book', 'posts_per_page' => -1]); } public function find($id) { return get_post($id); } }
SOLID Principles
SOLID represents five design principles for maintainable, scalable code: Single Responsibility, Open/Closed, Liskov Substitution, Interface Segregation, and Dependency Inversion.
// S - Single Responsibility: One class, one purpose class Post_Validator { // Only validates public function validate($data) { } } class Post_Repository { // Only handles storage public function save($post) { } } class Post_Notifier { // Only sends notifications public function notify($post) { } } // O - Open/Closed: Open for extension, closed for modification interface Export_Handler { public function export($data); } class CSV_Export implements Export_Handler { } class JSON_Export implements Export_Handler { } // Add PDF_Export without modifying existing code // L - Liskov Substitution: Subtypes must be substitutable class Bird { public function move() { } // Not fly()! Penguins can't fly } // I - Interface Segregation: Many specific interfaces interface Readable { public function read(); } interface Writable { public function write($data); } interface Deletable { public function delete(); } // Classes implement only what they need // D - Dependency Inversion: Depend on abstractions class Post_Service { public function __construct( Repository_Interface $repo, // Not concrete WP_Post_Repository Cache_Interface $cache // Not concrete Redis_Cache ) { } }
┌─────────────────────────────────────────────────┐
│ SOLID Principles │
├─────────────────────────────────────────────────┤
│ S │ Single Responsibility │
│ │ → One class = One reason to change │
├────┼────────────────────────────────────────────┤
│ O │ Open/Closed │
│ │ → Extend behavior without modifying code │
├────┼────────────────────────────────────────────┤
│ L │ Liskov Substitution │
│ │ → Subclasses replaceable for parents │
├────┼────────────────────────────────────────────┤
│ I │ Interface Segregation │
│ │ → Prefer small, specific interfaces │
├────┼────────────────────────────────────────────┤
│ D │ Dependency Inversion │
│ │ → Depend on abstractions, not concretes │
└────┴────────────────────────────────────────────┘
This completes Phase 13 covering object-oriented plugin development patterns essential for building professional, maintainable WordPress plugins at scale.
Composer
composer.json structure
The composer.json file is the central configuration for PHP dependency management, defining your project's metadata, dependencies, autoloading rules, and scripts in a declarative JSON format.
{ "name": "vendor/my-wordpress-plugin", "description": "My awesome WordPress plugin", "type": "wordpress-plugin", "license": "GPL-2.0-or-later", "authors": [ {"name": "Developer", "email": "dev@example.com"} ], "require": { "php": ">=7.4", "monolog/monolog": "^2.0" }, "require-dev": { "phpunit/phpunit": "^9.0", "wp-coding-standards/wpcs": "^2.3" }, "autoload": { "psr-4": { "MyPlugin\\": "src/" } }, "scripts": { "test": "phpunit", "lint": "phpcs" } }
Package installation
Composer installs packages from Packagist (or custom repositories) into the vendor/ directory, managing dependencies recursively and generating an autoloader; use composer install for exact versions from lock file or composer require to add new packages.
# Install all dependencies from composer.lock composer install # Add a new package composer require guzzlehttp/guzzle # Add dev dependency composer require --dev phpunit/phpunit # Update packages composer update # Remove a package composer remove guzzlehttp/guzzle
┌─────────────────────────────────────────────────────┐
│ composer require package │
└─────────────────────┬───────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────┐
│ Resolve dependencies recursively │
└─────────────────────┬───────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────┐
│ Download to vendor/ directory │
└─────────────────────┬───────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────┐
│ Update composer.lock with versions │
└─────────────────────┬───────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────┐
│ Regenerate autoloader │
└─────────────────────────────────────────────────────┘
Autoloader generation
Composer generates a PSR-4 compliant autoloader that automatically loads PHP classes when they're first used, eliminating manual require statements; include vendor/autoload.php once in your plugin's main file to enable autoloading for all dependencies and your own namespaced code.
<?php // In your main plugin file require_once __DIR__ . '/vendor/autoload.php'; // Now classes autoload automatically use MyPlugin\Core\Plugin; use MyPlugin\Admin\Settings; use Monolog\Logger; // composer.json autoload configuration
{ "autoload": { "psr-4": { "MyPlugin\\": "src/" }, "classmap": [ "includes/legacy/" ], "files": [ "src/helpers.php" ] } }
project/
├── src/
│ ├── Core/
│ │ └── Plugin.php → MyPlugin\Core\Plugin
│ ├── Admin/
│ │ └── Settings.php → MyPlugin\Admin\Settings
│ └── helpers.php → Always loaded
├── includes/legacy/
│ └── OldClass.php → Classmap scanned
└── vendor/
└── autoload.php → Generated autoloader
Scripts
Composer scripts allow you to define custom commands that run PHP code, shell commands, or other Composer commands, perfect for automating testing, linting, building, and deployment tasks within your WordPress development workflow.
{ "scripts": { "test": "phpunit", "test:coverage": "phpunit --coverage-html coverage", "lint": "phpcs --standard=WordPress src/", "lint:fix": "phpcbf --standard=WordPress src/", "analyze": "phpstan analyse src/", "build": [ "@lint", "@test", "npm run build" ], "post-install-cmd": [ "MyPlugin\\Composer\\Setup::postInstall" ], "post-update-cmd": [ "@build" ] }, "scripts-descriptions": { "test": "Run PHPUnit tests", "lint": "Check code standards" } }
# Run scripts composer test composer lint:fix composer build # List available scripts composer list
Version constraints
Version constraints define which package versions are acceptable using semantic versioning operators; understanding these ensures stable dependencies while allowing safe updates through the lock file mechanism.
{ "require": { "vendor/exact": "1.2.3", "vendor/range": ">=1.0 <2.0", "vendor/wildcard": "1.0.*", "vendor/tilde": "~1.2.3", "vendor/caret": "^1.2.3", "vendor/or": "1.0|2.0", "vendor/stability": "1.0@beta", "vendor/branch": "dev-main" } }
┌──────────────┬─────────────────────────────────────────┐
│ Constraint │ Meaning │
├──────────────┼─────────────────────────────────────────┤
│ 1.2.3 │ Exact version only │
│ >=1.0 <2.0 │ Range: 1.0 to 1.x only │
│ 1.0.* │ Any 1.0.x version │
│ ~1.2.3 │ >=1.2.3 <1.3.0 (patch updates) │
│ ^1.2.3 │ >=1.2.3 <2.0.0 (minor updates) ★ │
│ ^0.3.0 │ >=0.3.0 <0.4.0 (special for 0.x) │
└──────────────┴─────────────────────────────────────────┘
★ Most commonly used
Private packages
Private packages allow you to use proprietary code or premium WordPress components without publishing to Packagist; configure private repositories using VCS, path, or artifact types with authentication tokens stored in auth.json.
{ "repositories": [ { "type": "vcs", "url": "git@github.com:company/private-package.git" }, { "type": "path", "url": "../shared-library", "options": { "symlink": true } }, { "type": "composer", "url": "https://packages.company.com" }, { "type": "package", "package": { "name": "vendor/premium-plugin", "version": "1.0.0", "dist": { "url": "https://example.com/plugin.zip", "type": "zip" } } } ], "require": { "company/private-package": "^1.0" } }
# auth.json for credentials (don't commit!) { "http-basic": { "packages.company.com": { "username": "token", "password": "secret-token" } }, "github-oauth": { "github.com": "ghp_xxxxxxxxxxxx" } }
Packagist
Packagist is the default and primary public repository for PHP packages where Composer searches for dependencies; you can publish your own open-source WordPress libraries there or configure alternative repositories like WPackagist for WordPress-specific components.
┌─────────────────────────────────────────────────────────────┐
│ Composer Repositories │
├─────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────┐ Primary PHP package repository │
│ │ Packagist │ https://packagist.org │
│ │ (default) │ ~350,000 packages │
│ └─────────────┘ │
│ │
│ ┌─────────────┐ WordPress plugins/themes as packages │
│ │ WPackagist │ https://wpackagist.org │
│ └─────────────┘ │
│ │
│ ┌─────────────┐ Private/Enterprise solutions │
│ │ Private │ Satis, Toran Proxy, Private Packagist │
│ └─────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘
# Search Packagist composer search wordpress # Show package info composer show monolog/monolog # Publish your package: # 1. Push to GitHub # 2. Submit URL at packagist.org # 3. Setup webhook for auto-updates
WordPress-specific packages
WPackagist mirrors the WordPress.org plugin and theme repositories as Composer packages, enabling you to manage WordPress core, plugins, and themes as dependencies with version constraints and automated updates.
{ "repositories": [ { "type": "composer", "url": "https://wpackagist.org" } ], "require": { "php": ">=7.4", "composer/installers": "^2.0", "johnpbloch/wordpress": "^6.4", "wpackagist-plugin/woocommerce": "^8.0", "wpackagist-plugin/advanced-custom-fields": "^6.0", "wpackagist-theme/developer": "^2.0" }, "extra": { "installer-paths": { "wp-content/plugins/{$name}/": [ "type:wordpress-plugin" ], "wp-content/themes/{$name}/": [ "type:wordpress-theme" ], "wp-content/mu-plugins/{$name}/": [ "type:wordpress-muplugin" ] }, "wordpress-install-dir": "wp" } }
project/
├── composer.json
├── wp/ ← WordPress core
│ ├── wp-admin/
│ └── wp-includes/
├── wp-content/
│ ├── plugins/
│ │ ├── woocommerce/ ← From WPackagist
│ │ └── my-custom-plugin/ ← Your plugin
│ └── themes/
└── vendor/
npm for WordPress
package.json
The package.json file defines your project's JavaScript dependencies, scripts, and metadata; for WordPress development, it typically includes @wordpress/scripts and related packages for building Gutenberg blocks and modern JavaScript assets.
{ "name": "my-wordpress-plugin", "version": "1.0.0", "description": "WordPress plugin with block editor support", "author": "Developer", "license": "GPL-2.0-or-later", "main": "build/index.js", "scripts": { "build": "wp-scripts build", "start": "wp-scripts start", "lint:js": "wp-scripts lint-js", "lint:css": "wp-scripts lint-style", "test": "wp-scripts test-unit-js", "packages-update": "wp-scripts packages-update" }, "devDependencies": { "@wordpress/scripts": "^26.0.0" }, "dependencies": { "@wordpress/block-editor": "^12.0.0", "@wordpress/blocks": "^12.0.0", "@wordpress/i18n": "^4.0.0" } }
@wordpress/scripts
@wordpress/scripts is WordPress's official zero-config build tool that wraps webpack, Babel, ESLint, and other tools with sensible defaults for developing blocks, scripts, and styles; it handles JSX transformation, CSS processing, and dependency extraction automatically.
# Install npm install @wordpress/scripts --save-dev # Available commands npx wp-scripts build # Production build npx wp-scripts start # Development with watch npx wp-scripts lint-js # ESLint npx wp-scripts lint-style # Stylelint npx wp-scripts test-unit-js # Jest tests npx wp-scripts format # Prettier formatting npx wp-scripts packages-update # Update WP packages
┌─────────────────────────────────────────────────────────┐
│ @wordpress/scripts includes │
├─────────────────────────────────────────────────────────┤
│ Webpack 5 │ Module bundling │
│ Babel │ JSX/ES6+ transpilation │
│ PostCSS │ CSS processing │
│ Sass │ SCSS compilation │
│ ESLint │ JavaScript linting │
│ Stylelint │ CSS/SCSS linting │
│ Prettier │ Code formatting │
│ Jest │ JavaScript testing │
│ Dependency │ Auto wp_enqueue dependencies │
│ Extraction │ │
└─────────────────────────────────────────────────────────┘
Build process
The build process compiles your source files from src/ to build/, transforming modern JavaScript/JSX into browser-compatible code, processing SCSS/CSS, and generating a .asset.php file containing dependency and version information for proper WordPress enqueueing.
# Directory structure src/ ├── index.js # Entry point ├── edit.js # Block edit component ├── save.js # Block save component ├── style.scss # Frontend styles └── editor.scss # Editor-only styles # After build build/ ├── index.js # Bundled JavaScript ├── index.asset.php # Dependencies & version ├── style-index.css # Processed frontend CSS └── index.css # Processed editor CSS
<?php // Using generated asset file $asset = include plugin_dir_path(__FILE__) . 'build/index.asset.php'; wp_register_script( 'my-block', plugins_url('build/index.js', __FILE__), $asset['dependencies'], // ['wp-blocks', 'wp-element', ...] $asset['version'], // Auto-generated hash true );
Development mode
Development mode (npm start or wp-scripts start) watches your source files for changes and rebuilds automatically with source maps enabled for debugging; this provides fast iteration during development with unminified output for easier troubleshooting.
# Start development mode npm start # Or with specific entry npx wp-scripts start src/blocks/my-block/index.js
┌─────────────────────────────────────────────────────────┐
│ Development Mode Features │
├─────────────────────────────────────────────────────────┤
│ ✓ File watching - Auto-rebuild on changes │
│ ✓ Source maps - Debug original source │
│ ✓ Unminified output - Readable code │
│ ✓ Fast rebuilds - Incremental compilation │
│ ✓ Error overlay - Clear error messages │
└─────────────────────────────────────────────────────────┘
Terminal output:
┌──────────────────────────────────────────────────────┐
│ $ npm start │
│ │
│ asset index.js 42.5 KiB [emitted] (name: index) │
│ asset style-index.css 1.2 KiB [emitted] │
│ │
│ webpack 5.88.0 compiled successfully in 1523 ms │
│ │
│ Watching for changes... │
└──────────────────────────────────────────────────────┘
Production builds
Production builds (npm run build) create optimized, minified output with tree-shaking to eliminate dead code, asset versioning for cache busting, and extracted WordPress dependencies to avoid bundling core libraries that WordPress already provides.
# Create production build npm run build # Output comparison ┌─────────────────────────────────────────────────────────┐ │ Development vs Production │ ├────────────────────┬────────────────────────────────────┤ │ Development │ Production │ ├────────────────────┼────────────────────────────────────┤ │ index.js: 250 KB │ index.js: 45 KB (minified) │ │ Source maps: Yes │ Source maps: No │ │ Comments: Yes │ Comments: Stripped │ │ Dead code: Yes │ Tree-shaken │ │ Build time: Fast │ Build time: Slower │ └────────────────────┴────────────────────────────────────┘
{ "scripts": { "build": "wp-scripts build", "build:analyze": "wp-scripts build --webpack-bundle-analyzer" } }
Hot module replacement
Hot Module Replacement (HMR) allows updating JavaScript modules in the browser without a full page reload, preserving application state during development; for WordPress, this requires additional setup with webpack-dev-server or tools like BrowserSync.
// webpack.config.js - Custom HMR setup const defaultConfig = require('@wordpress/scripts/config/webpack.config'); module.exports = { ...defaultConfig, devServer: { hot: true, port: 8887, allowedHosts: 'all', headers: { 'Access-Control-Allow-Origin': '*', }, proxy: { '/': { target: 'http://localhost:8888', // wp-env URL changeOrigin: true, }, }, }, };
┌─────────────────────────────────────────────────────────┐
│ HMR Flow │
│ │
│ ┌──────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ Edit │───▶│ Webpack │───▶│ Browser │ │
│ │ Code │ │ Dev Server │ │ Updates │ │
│ └──────────┘ └──────────────┘ └──────────────┘ │
│ │ │
│ ▼ │
│ WebSocket Push │
│ (No full reload) │
└─────────────────────────────────────────────────────────┘
Custom webpack config
Extend the default @wordpress/scripts webpack configuration by creating a webpack.config.js that imports and spreads the default config, then adds or overrides specific settings like entry points, aliases, loaders, or plugins.
// webpack.config.js const defaultConfig = require('@wordpress/scripts/config/webpack.config'); const path = require('path'); module.exports = { ...defaultConfig, // Multiple entry points entry: { 'block-one': './src/blocks/block-one/index.js', 'block-two': './src/blocks/block-two/index.js', 'admin': './src/admin/index.js', 'frontend': './src/frontend/index.js', }, // Path aliases resolve: { ...defaultConfig.resolve, alias: { ...defaultConfig.resolve?.alias, '@components': path.resolve(__dirname, 'src/components'), '@hooks': path.resolve(__dirname, 'src/hooks'), '@utils': path.resolve(__dirname, 'src/utils'), }, }, // Additional plugins plugins: [ ...defaultConfig.plugins, // Your custom plugins ], };
// Usage with aliases import { Button } from '@components/Button'; import { useSettings } from '@hooks/useSettings'; import { formatDate } from '@utils/helpers';