WordPress Plugin Boilerplate Setup
A modern WordPress plugin boilerplate — autoloading, namespaces, build pipeline, activation/deactivation, i18n, and the structure that scales from one file to a real product.
A single-file plugin is fine for an afternoon experiment. Past that, you need structure — autoloading, namespaces, a build pipeline, activation/deactivation hooks, i18n, settings persistence, and an opinionated layout that doesn't fight you at scale. This is the boilerplate we start from for any new client plugin or product. It's opinionated; you can and should adapt it to your team's conventions.
The directory layout
mxt-bookings/
├── mxt-bookings.php ← bootstrap (only the entry point)
├── composer.json ← PSR-4 autoload
├── package.json ← @wordpress/scripts build
├── readme.txt ← wp.org-compatible readme
├── src/
│ ├── Plugin.php ← main class, hook wiring
│ ├── Activator.php ← activation hook
│ ├── Deactivator.php ← deactivation hook
│ ├── Admin/
│ │ ├── Menu.php
│ │ ├── Settings.php
│ │ └── views/
│ ├── Frontend/
│ │ └── Shortcodes.php
│ ├── PostTypes/
│ │ └── Booking.php
│ ├── Rest/
│ │ └── Controller.php
│ └── Util/
│ └── Logger.php
├── assets/
│ ├── src/ ← block source, JS source
│ │ ├── blocks/
│ │ └── admin/
│ └── build/ ← compiled output (gitignored or tracked)
├── languages/
│ └── mxt-bookings.pot
├── tests/
│ └── Unit/
└── vendor/ ← composer (gitignored)A few principles worth surfacing:
- One thing per file. A class per file. The class name matches the filename. PSR-4 autoloading does the rest.
- The bootstrap PHP is small. The main plugin file should do almost nothing — load the autoloader, instantiate the main class, register activation/deactivation hooks. All the actual logic lives in
src/. - Source vs build separation.
assets/src/is JavaScript/CSS source;assets/build/is the compiled output your code actually loads.
The bootstrap file
<?php
/**
* Plugin Name: MXT Bookings
* Plugin URI: https://maxtdesign.com/plugins/mxt-bookings
* Description: A bookings plugin for WordPress.
* Version: 1.0.0
* Requires at least: 6.4
* Requires PHP: 8.1
* Author: MaxtDesign
* Author URI: https://maxtdesign.com
* License: GPL v2 or later
* Text Domain: mxt-bookings
* Domain Path: /languages
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
define( 'MXT_BOOKINGS_VERSION', '1.0.0' );
define( 'MXT_BOOKINGS_FILE', __FILE__ );
define( 'MXT_BOOKINGS_DIR', plugin_dir_path( __FILE__ ) );
define( 'MXT_BOOKINGS_URL', plugin_dir_url( __FILE__ ) );
require_once MXT_BOOKINGS_DIR . 'vendor/autoload.php';
register_activation_hook( __FILE__, [ \Mxt\Bookings\Activator::class, 'activate' ] );
register_deactivation_hook( __FILE__, [ \Mxt\Bookings\Deactivator::class, 'deactivate' ] );
add_action( 'plugins_loaded', static function () {
( new \Mxt\Bookings\Plugin() )->init();
} );The plugin header at the top is the only WordPress-specific metadata required. Everything else is namespaced PHP.
composer.json — autoloading the right way
{
"name": "maxtdesign/mxt-bookings",
"description": "A bookings plugin for WordPress.",
"type": "wordpress-plugin",
"license": "GPL-2.0-or-later",
"require": {
"php": ">=8.1"
},
"require-dev": {
"phpunit/phpunit": "^10",
"wp-coding-standards/wpcs": "^3.0"
},
"autoload": {
"psr-4": {
"Mxt\\Bookings\\": "src/"
}
},
"autoload-dev": {
"psr-4": {
"Mxt\\Bookings\\Tests\\": "tests/"
}
},
"config": {
"optimize-autoloader": true,
"preferred-install": "dist"
}
}PSR-4 means: the namespace prefix maps to a directory. Mxt\Bookings\Admin\Menu lives at src/Admin/Menu.php. No more manual require_once.
Run composer install --no-dev for production builds. Commit composer.lock. Add vendor/ to .gitignore for plugin repos you control; if shipping to wp.org, include the vendor directory in the deploy package.
The main Plugin class
<?php
namespace Mxt\Bookings;
use Mxt\Bookings\Admin\Menu;
use Mxt\Bookings\Admin\Settings;
use Mxt\Bookings\PostTypes\Booking;
use Mxt\Bookings\Rest\Controller;
use Mxt\Bookings\Frontend\Shortcodes;
final class Plugin {
public function init(): void {
// i18n
load_plugin_textdomain(
'mxt-bookings',
false,
dirname( plugin_basename( MXT_BOOKINGS_FILE ) ) . '/languages'
);
// Module wiring — each module registers its own hooks.
( new Booking() )->register();
if ( is_admin() ) {
( new Menu() )->register();
( new Settings() )->register();
}
( new Shortcodes() )->register();
( new Controller() )->register();
}
}Each module is a class with a register() method that does its own add_action() / add_filter() calls. The Plugin class wires modules, nothing more.
For deeper hook patterns inside each module, see Understanding WordPress Hooks.
Activation and deactivation
Activation is the right place to: register your custom post types (then flush rewrite rules), set default options, create required database tables. Deactivation should clean up rewrite rules and scheduled events. Uninstall (a separate uninstall.php) handles permanent data removal.
<?php
namespace Mxt\Bookings;
final class Activator {
public static function activate(): void {
// Default options
add_option( 'mxt_bookings_settings', [ 'auto_confirm' => false ] );
// Custom tables (if needed)
self::ensure_db_schema();
// Register CPT, then flush rewrites for the new permalinks
( new \Mxt\Bookings\PostTypes\Booking() )->register();
flush_rewrite_rules();
}
private static function ensure_db_schema(): void {
global $wpdb;
$charset = $wpdb->get_charset_collate();
$table = $wpdb->prefix . 'mxt_bookings';
$sql = "CREATE TABLE {$table} (
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
customer_email VARCHAR(190) NOT NULL,
booking_date DATE NOT NULL,
status VARCHAR(32) NOT NULL DEFAULT 'pending',
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
INDEX idx_email (customer_email),
INDEX idx_status_date (status, booking_date)
) {$charset};";
require_once ABSPATH . 'wp-admin/includes/upgrade.php';
dbDelta( $sql );
}
}Always use dbDelta() for schema changes — it generates safe ALTER statements rather than dropping tables. Run a schema-version check on every plugin load to handle upgrades cleanly.
The build pipeline
Use @wordpress/scripts for the JavaScript build — it ships a pre-configured webpack with the right Babel preset and React mode for blocks.
{
"name": "mxt-bookings",
"version": "1.0.0",
"scripts": {
"build": "wp-scripts build --webpack-src-dir=assets/src --output-path=assets/build",
"start": "wp-scripts start --webpack-src-dir=assets/src --output-path=assets/build",
"make-pot": "wp-scripts i18n:make-pot . languages/mxt-bookings.pot",
"lint:js": "wp-scripts lint-js assets/src",
"lint:php": "phpcs --standard=WordPress src/"
},
"devDependencies": {
"@wordpress/scripts": "^28.0.0"
}
}Settings — the right way
Use the WordPress Settings API rather than rolling your own form handler. It handles nonces, capabilities, and persistence for you:
namespace Mxt\Bookings\Admin;
final class Settings {
public function register(): void {
add_action( 'admin_init', [ $this, 'register_settings' ] );
}
public function register_settings(): void {
register_setting(
'mxt_bookings_group',
'mxt_bookings_settings',
[
'type' => 'object',
'sanitize_callback' => [ $this, 'sanitize' ],
'show_in_rest' => false,
'default' => [ 'auto_confirm' => false ],
]
);
add_settings_section( 'mxt_bookings_main', 'Bookings', null, 'mxt-bookings' );
add_settings_field(
'auto_confirm',
'Auto-confirm bookings',
[ $this, 'render_auto_confirm' ],
'mxt-bookings',
'mxt_bookings_main'
);
}
public function sanitize( $input ): array {
return [
'auto_confirm' => ! empty( $input['auto_confirm'] ),
];
}
public function render_auto_confirm(): void {
$opts = get_option( 'mxt_bookings_settings' );
?>
<input type="checkbox" name="mxt_bookings_settings[auto_confirm]"
value="1" <?php checked( $opts['auto_confirm'] ?? false ); ?> />
<?php
}
}Internationalization
Wrap user-facing strings in __(), _e(), esc_html__(). Use the text domain that matches the plugin slug. Run npm run make-pot to extract translatable strings into languages/mxt-bookings.pot. Translators take it from there.
Don't skip i18n on the assumption your plugin is English-only. WP-CLI sites and WordPress.com properties auto- translate plugins where the strings are wrapped — yours can't participate if you didn't wrap.
Testing
Two layers of test for plugins:
- Unit testswith PHPUnit — for logic that doesn't need WordPress (sanitizers, formatters, business calculations). Fastest feedback loop.
- Integration tests with
wp-phpunit— for code that calls into WordPress functions. Requires a test WP install; slower but catches real interactions.
The deeper debugging toolkit is in Debugging WordPress Plugins Like a Pro.
Coding standards
Run phpcs --standard=WordPress src/on commit (via a pre-commit hook or CI). The WordPress coding standards codify hundreds of small consistency rules — alignment, brace style, escaping, prefix conventions. They're not aesthetic; they catch real bugs (unprefixed globals, missing escaping).
Add a phpcs.xml.dist to root with sensible exceptions for your project; check it in.
Distribution
Two paths:
- WordPress.org — the canonical channel for free plugins. Requires GPL license, careful review of your
readme.txt, and patience with the SVN-based workflow. - Self-hosted — your own site, with EDD or Freemius for paid plugins. Update server via
plugin_update_checkfilter or a library like Plugin Update Checker. More work, more control.
The boilerplate above is what we use to start any new MaxtDesign plugin (including the four currently shipping — /plugins). It's opinionated and not the only valid shape, but it scales from one developer to a team without rewrites. From here, the natural follow-ups: see custom Gutenberg blocks for adding blocks to this scaffold, and hooks and filters for the deeper extension patterns. For plugins that warrant senior engineering effort, our plugin development service handles the full lifecycle.
Need help putting this into practice?
MaxtDesign builds the AI-powered web stacks the articles describe — from agentic workflows to performance-first WordPress + WooCommerce. Talk to us about your project.