Plugin Development
This document defines the company standard for structuring and architecting WordPress plugins. It is based on established patterns (including ThemeSwitcher Pro) and should be followed for all new plugin development.
The source export also contained theme-focused material; that content now lives under Theme Development so this page stays scoped to plugins.
Directory Structure
All plugins must follow this directory structure:
plugin-name/
├── plugin-name.php # Main plugin entry point
├── index.php # Security file ("Silence is golden")
├── composer.json # PHP dependencies and scripts
├── package.json # Node dependencies and build scripts
├── webpack.global.js # Webpack configuration
├── phpcs.xml.dist # PHP CodeSniffer configuration
├── .editorconfig # Editor configuration
├── .prettierrc.js # Prettier configuration
├── .stylelintrc.json # Stylelint configuration
│
├── admin/ # Admin-only code
│ ├── class-{prefix}-admin.php # Admin orchestrator class
│ ├── partials/ # Admin UI components
│ │ ├── templates/ # Admin template files
│ │ └── class-*.php # Admin partial classes
│ ├── css/ # Built admin CSS
│ └── js/ # Built admin JS
│
├── public/ # Frontend-only code
│ ├── class-{prefix}-public.php
│ ├── css/ # Built public CSS
│ └── js/ # Built public JS
│
├── inc/ # Core logic (shared between admin/public)
│ ├── class-{prefix}.php # Main plugin class
│ ├── class-{prefix}-loader.php # Hook registry
│ ├── class-{prefix}-i18n.php # Internationalization
│ ├── class-{prefix}-activator.php
│ ├── class-{prefix}-deactivator.php
│ ├── class-{prefix}-helper.php # Singleton helper utilities
│ ├── handler-classes/ # Request/data handlers
│ └── addons/ # Third-party integrations
│
├── assets/ # Source files (pre-build)
│ ├── js/ # Source JavaScript
│ └── scss/ # Source SCSS
│
├── languages/ # Translation files
│ └── plugin-name.pot # POT template
│
├── docs/ # Documentation
└── build/ # Build output (gitignored)
Security index files
Every directory must contain an index.php file with:
<?php
// Silence is golden.
This prevents directory listing on servers with directory browsing enabled.
PHP class organization
Naming conventions
| Element | Convention | Example |
|---|---|---|
| Class prefix | 2–4 letter uppercase abbreviation | TSP_, ABC_ |
| Class names | {PREFIX}_{Descriptive_Name} | TSP_Admin, TSP_Settings |
| File names | class-{prefix}-{name}.php | class-tsp-admin.php |
| Hook prefix | {prefix}_filter_, {prefix}_action_ | tsp_filter_themes, tsp_action_init |
Class loading
Classes are loaded via require_once or include_once. Load classes in the main plugin class's load_dependencies() method:
private function load_dependencies() {
include_once plugin_dir_path( __DIR__ ) . 'inc/class-{prefix}-loader.php';
include_once plugin_dir_path( __DIR__ ) . 'admin/class-{prefix}-admin.php';
include_once plugin_dir_path( __DIR__ ) . 'public/class-{prefix}-public.php';
// Additional dependencies...
}
DocBlocks
All classes must include proper DocBlocks:
<?php
/**
* The admin-specific functionality of the plugin.
*
* @package Plugin_Name
* @subpackage Plugin_Name/admin
* @author Company Name <email@example.com>
* @since 1.0.0
*/
/**
* Class {PREFIX}_Admin
*
* Handles all admin-side functionality.
*
* @since 1.0.0
*/
class PREFIX_Admin {
// ...
}
Singleton pattern
For utility/helper classes that should only be instantiated once:
class PREFIX_Helper {
private static $instance = null;
public static function get_instance() {
if ( null === self::$instance ) {
self::$instance = new self();
}
return self::$instance;
}
private function __construct() {
// Private constructor
}
}
Entry point and initialization
Main plugin file structure
The main plugin file (plugin-name.php) must follow this structure:
<?php
/**
* Plugin Name: Plugin Name
* Plugin URI: https://example.com/plugin
* Description: Brief description of the plugin.
* Version: 1.0.0
* Requires at least: 6.0
* Requires PHP: 8.0
* Author: Company Name
* Author URI: https://example.com
* License: GPL-2.0+
* License URI: http://www.gnu.org/licenses/gpl-2.0.txt
* Text Domain: plugin-name
* Domain Path: /languages
*/
// Abort if called directly.
if ( ! defined( 'WPINC' ) ) {
die;
}
// PHP version check.
if ( version_compare( PHP_VERSION, '8.0', '<' ) ) {
add_action( 'admin_notices', function() {
echo '<div class="error"><p>';
esc_html_e( 'Plugin Name requires PHP 8.0 or higher.', 'plugin-name' );
echo '</p></div>';
});
return;
}
// Define plugin constants.
define( 'PREFIX_VERSION', '1.0.0' );
define( 'PREFIX_FILE', __FILE__ );
define( 'PREFIX_DIR', plugin_dir_path( __FILE__ ) );
define( 'PREFIX_URL', plugin_dir_url( __FILE__ ) );
// Load dependencies.
require_once PREFIX_DIR . 'inc/class-prefix-activator.php';
require_once PREFIX_DIR . 'inc/class-prefix-deactivator.php';
require_once PREFIX_DIR . 'inc/class-prefix.php';
// Activation/Deactivation hooks.
register_activation_hook( __FILE__, [ 'PREFIX_Activator', 'activate' ] );
register_deactivation_hook( __FILE__, [ 'PREFIX_Deactivator', 'deactivate' ] );
/**
* Begin plugin execution.
*
* @since 1.0.0
*/
function run_prefix() {
$plugin = new PREFIX();
$plugin->run();
}
run_prefix();
Main plugin class
class PREFIX {
protected $loader;
protected $plugin_name;
protected $version;
public function __construct() {
$this->plugin_name = 'plugin-name';
$this->version = PREFIX_VERSION;
$this->load_dependencies();
$this->set_locale();
$this->define_admin_hooks();
$this->define_public_hooks();
}
private function load_dependencies() {
require_once PREFIX_DIR . 'inc/class-prefix-loader.php';
require_once PREFIX_DIR . 'inc/class-prefix-i18n.php';
require_once PREFIX_DIR . 'admin/class-prefix-admin.php';
require_once PREFIX_DIR . 'public/class-prefix-public.php';
$this->loader = new PREFIX_Loader();
}
private function set_locale() {
$i18n = new PREFIX_i18n();
$this->loader->add_action( 'plugins_loaded', $i18n, 'load_plugin_textdomain' );
}
private function define_admin_hooks() {
$admin = new PREFIX_Admin( $this->plugin_name, $this->version );
$this->loader->add_action( 'admin_enqueue_scripts', $admin, 'enqueue_styles' );
$this->loader->add_action( 'admin_enqueue_scripts', $admin, 'enqueue_scripts' );
}
private function define_public_hooks() {
$public = new PREFIX_Public( $this->plugin_name, $this->version );
$this->loader->add_action( 'wp_enqueue_scripts', $public, 'enqueue_styles' );
$this->loader->add_action( 'wp_enqueue_scripts', $public, 'enqueue_scripts' );
}
public function run() {
$this->loader->run();
}
}
Hook loader class
Use a centralized hook loader for better organization:
class PREFIX_Loader {
protected $actions = [];
protected $filters = [];
public function add_action( $hook, $component, $callback, $priority = 10, $accepted_args = 1 ) {
$this->actions = $this->add( $this->actions, $hook, $component, $callback, $priority, $accepted_args );
}
public function add_filter( $hook, $component, $callback, $priority = 10, $accepted_args = 1 ) {
$this->filters = $this->add( $this->filters, $hook, $component, $callback, $priority, $accepted_args );
}
private function add( $hooks, $hook, $component, $callback, $priority, $accepted_args ) {
$hooks[] = [
'hook' => $hook,
'component' => $component,
'callback' => $callback,
'priority' => $priority,
'accepted_args' => $accepted_args,
];
return $hooks;
}
public function run() {
foreach ( $this->filters as $hook ) {
add_filter( $hook['hook'], [ $hook['component'], $hook['callback'] ], $hook['priority'], $hook['accepted_args'] );
}
foreach ( $this->actions as $hook ) {
add_action( $hook['hook'], [ $hook['component'], $hook['callback'] ], $hook['priority'], $hook['accepted_args'] );
}
}
}
Feature and module separation
Handler classes
Group related functionality into handler classes in inc/handler-classes/:
inc/handler-classes/
├── class-prefix-request-handler.php # HTTP request handling
├─ ─ class-prefix-post-handler.php # Post type operations
├── class-prefix-taxonomy-handler.php # Taxonomy operations
└── class-prefix-transient-handler.php # Caching/transients
Addons and integrations
Third-party integrations should use an abstract base class:
// inc/addons/class-prefix-addon-base.php
abstract class PREFIX_Addon_Base {
abstract public function init();
abstract public function is_active();
protected function register_hooks() {
// Common hook registration
}
}
// inc/addons/class-prefix-woocommerce.php
class PREFIX_WooCommerce extends PREFIX_Addon_Base {
public function is_active() {
return class_exists( 'WooCommerce' );
}
public function init() {
if ( ! $this->is_active() ) {
return;
}
$this->register_hooks();
}
}
Admin vs frontend separation
Admin class
The admin class handles all WordPress admin functionality:
class PREFIX_Admin {
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,
plugin_dir_url( __FILE__ ) . 'css/prefix-admin.css',
[],
$this->version,
'all'
);
}
public function enqueue_scripts() {
wp_enqueue_script(
$this->plugin_name,
plugin_dir_url( __FILE__ ) . 'js/prefix-admin.js',
[ 'jquery' ],
$this->version,
true
);
wp_localize_script(
$this->plugin_name,
'prefixAdminObject',
[
'ajaxurl' => admin_url( 'admin-ajax.php' ),
'nonce' => wp_create_nonce( 'prefix-admin-nonce' ),
]
);
}
}
Public class
The public class handles frontend functionality:
class PREFIX_Public {
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,
plugin_dir_url( __FILE__ ) . 'css/prefix-public.css',
[],
$this->version,
'all'
);
}
public function enqueue_scripts() {
wp_enqueue_script(
$this->plugin_name,
plugin_dir_url( __FILE__ ) . 'js/prefix-public.js',
[ 'jquery' ],
$this->version,
true
);
}
}
Asset handling
Source file structure
assets/
├── js/
│ ├── admin.js # Admin JavaScript entry point
│ └── frontend.js # Frontend JavaScript entry point
└── scss/
├── admin-styles.scss # Admin SCSS entry point
├── frontend-styles.scss # Frontend SCSS entry point
└── _partials/ # SCSS partials
├── _variables.scss
├── _mixins.scss
└── _components.scss
Webpack configuration
Use @wordpress/scripts with custom webpack configuration:
// webpack.global.js
const defaultConfig = require( '@wordpress/scripts/config/webpack.config' );
const MiniCssExtractPlugin = require( 'mini-css-extract-plugin' );
const path = require( 'path' );
module.exports = {
...defaultConfig,
entry: {
admin: './assets/js/admin.js',
frontend: './assets/js/frontend.js',
},
output: {
path: path.resolve( __dirname, 'build' ),
filename: '[name].js',
},
module: {
rules: [
...defaultConfig.module.rules,
{
test: /\.scss$/,
use: [
MiniCssExtractPlugin.loader,
'css-loader',
'sass-loader',
],
},
],
},
plugins: [
...defaultConfig.plugins,
new MiniCssExtractPlugin( {
filename: '[name].css',
} ),
],
};