Most WordPress plugin tutorials show you how to get something working in 50 lines.
That’s fine for learning, but when you’re shipping to real clients — with real edge cases,
real update cycles, and real support tickets — you need a different approach entirely.
This is what I’ve learned after building and maintaining several production plugins
for Japanese legal service firms and marketing teams.
Project Structure That Scales
The single biggest mistake I see in plugin codebases is treating functions.php
like a junk drawer. Everything goes in, nothing comes out cleanly. Instead, think of your
plugin as a small application with real separation of concerns.
plaintext
my-plugin/
├── my-plugin.php # Bootstrap only
├── includes/
│ ├── class-core.php # Main plugin class
│ ├── class-admin.php # WP Admin screens
│ ├── class-api.php # REST endpoints
│ └── class-db.php # DB abstraction
├── assets/
│ ├── js/
│ └── css/
└── templates/ # Frontend views
The main plugin file should do almost nothing — just load the autoloader and
instantiate the core class. No business logic, no hooks, no output.
Hook Architecture
WordPress is built on hooks. But managing them naively leads to impossible-to-debug
interactions where you can’t tell what’s registered or when. Use a pattern I call
deferred hook registration: collect all hooks in a manifest, register
them in a single pass at boot time.
php
class Plugin_Core {
private $hooks = [];
public function register_hook($type, $tag, $callback, $priority = 10) {
$this->hooks[] = compact('type', 'tag', 'callback', 'priority');
}
public function run() {
foreach ($this->hooks as $hook) {
call_user_func(
'add_' . $hook['type'],
$hook['tag'],
$hook['callback'],
$hook['priority']
);
}
}
}
The goal isn’t to write clever code. It’s to write code that your future self —
or a contractor — can understand at 11pm when something breaks in production.
Database Layer
Never write raw SQL scattered through your plugin. Always abstract it. Even if you’re
only doing simple queries today, you’ll thank yourself when you need to swap table
prefixes, add caching, or migrate to a custom post type.
php
class Plugin_DB {
private static $table;
public static function init() {
global $wpdb;
self::$table = $wpdb->prefix . 'my_plugin_data';
}
public static function get_entries($limit = 20) {
global $wpdb;
return $wpdb->get_results(
$wpdb->prepare(
"SELECT * FROM %i ORDER BY created_at DESC LIMIT %d",
self::$table, $limit
)
);
}
}
Security Fundamentals
Every form submission needs a nonce. Every AJAX handler needs capability checks.
Every output needs escaping. These are not optional — they’re the minimum bar
for a plugin that won’t embarrass you.
One pattern I enforce in every plugin: validate on the way in,
escape on the way out. Never trust user data anywhere in the stack,
and never output anything without an appropriate escape function.
Nonce Verification Pattern
php
function handle_form_submission() {
// 1. Verify nonce
if ( ! check_ajax_referer( 'my_plugin_action', 'nonce', false ) ) {
wp_send_json_error( ['msg' => 'Invalid nonce'], 403 );
}
// 2. Check capability
if ( ! current_user_can( 'manage_options' ) ) {
wp_send_json_error( ['msg' => 'Unauthorized'], 401 );
}
// 3. Sanitize input
$value = sanitize_text_field( $_POST['value'] ?? '' );
// 4. Do the work...
}
Takeaways
Building production WordPress plugins is mostly about discipline, not ingenuity.
The patterns that work are boring on purpose: structured files, explicit hooks,
abstracted data access, security by default.
The reward for that boring foundation is a codebase you can actually maintain —
and clients who don’t call you at midnight.
Most WordPress plugin tutorials show you how to get something working in 50 lines. That’s fine for learning, but when you’re shipping to real clients — with real edge cases, real update cycles, and real support tickets — you need a different approach entirely. This is what I’ve learned after building and maintaining several production…