Skip to content
William Alexander
  • Home
  • Case Studies
  • Personal Projects
  • Articles
  1. Home
  2. Articles
  3. WordPress Plugin Development Fundamentals
WordPress Enterprise

WordPress Plugin Development Fundamentals

Building custom functionality the WordPress way

May 3, 2025 13 min read

Key Takeaways

  • Plugins are the WordPress way to add custom functionality
  • The hooks system (actions and filters) is fundamental to plugin development
  • Follow WordPress coding standards for maintainable, compatible code
  • Security is critical—sanitize inputs, escape outputs, check capabilities
  • Build modularly so features can be enabled, disabled, and maintained independently
Overview

Why Build Plugins?

The first custom WordPress plugin I built was a simple form handler. A client needed a specific submission workflow that no existing plugin supported—not without bloat, awkward customizations, and features they'd never use. I could have hacked something into the theme's functions.php, but I'd learned that lesson the hard way on a previous project when a theme update wiped out custom functionality. So I built a plugin. Two hundred lines of focused code that did exactly what was needed and nothing more.

That plugin is still running five years later. The site has gone through three theme changes, but the form functionality works exactly as it did on day one. That's the power of plugins: they separate functionality from presentation, survive theme changes, and can be shared across sites. The WordPress plugin architecture isn't just a technical convenience—it's a fundamental design philosophy that keeps sites maintainable over time.

Whether you're building for a single site or distributing to thousands, understanding plugin development fundamentals makes you a more effective WordPress developer. The patterns you learn apply not just to plugins but to building quality WordPress code generally. Hooks, security practices, database operations, admin interfaces—these are the building blocks of everything WordPress does.

When to Build a Plugin

Not everything needs to be a plugin, but many things should be. Build a plugin when you're adding functionality that isn't theme-specific—if switching themes shouldn't break the feature, it belongs in a plugin. Custom post types and taxonomies are the classic example: if you build a "portfolio" post type in your theme, switching themes makes your portfolio content disappear. Build it in a plugin, and your content persists no matter what theme you use.

Plugins are also the right choice for integrations with external services, admin tools and dashboards, and any functionality you might want to reuse across projects. If you find yourself copying code from functions.php to functions.php across sites, that's a strong signal you should package it as a plugin.

Plugin vs. Theme Functions

If functionality is presentation-related, put it in your theme. If it's functionality that should persist regardless of theme, put it in a plugin. Custom post types, for example, belong in plugins—switching themes shouldn't make your content disappear.

Structure

Plugin Structure

A well-organized plugin is easier to maintain and extend. The structure you choose on day one affects how painful the plugin is to work with on day one thousand. I've inherited plugins that were single files with two thousand lines of spaghetti code, and I've worked with plugins that were over-engineered to the point of absurdity. The sweet spot is a structure that scales appropriately for your plugin's complexity.

Minimum Requirements

A WordPress plugin needs only one file with a proper header comment. That's it. WordPress reads the header to identify the plugin, and everything else is up to you. Here's the simplest possible plugin:

<?php
/**
 * Plugin Name: My Custom Plugin
 * Description: A brief description of what this plugin does.
 * Version: 1.0.0
 * Author: Your Name
 * License: GPL-2.0+
 */

// Plugin code here

For a simple utility plugin that does one thing, this might be all you need. I have several small plugins that are single files under a hundred lines. They register a shortcode, add a small admin feature, or modify a specific behavior. No need for elaborate structure when the problem is simple.

Recommended Structure

As plugins grow in complexity, organization becomes crucial. The structure I use for most serious plugins separates concerns into logical directories and files. This isn't the only valid approach, but it's proven to work well for plugins of moderate to significant complexity.

my-plugin/
├── my-plugin.php           # Main plugin file
├── readme.txt              # WordPress.org readme
├── uninstall.php           # Cleanup on uninstall
├── includes/
│   ├── class-plugin.php    # Main plugin class
│   ├── class-admin.php     # Admin functionality
│   └── class-public.php    # Public functionality
├── admin/
│   ├── css/
│   ├── js/
│   └── views/
├── public/
│   ├── css/
│   └── js/
└── languages/              # Translation files

The main plugin file stays lean—it defines constants, includes the main class, and initializes the plugin. All the real work happens in the classes under includes/. Admin-specific assets and views live in the admin/ directory; public-facing assets live in public/. This separation makes it easy to enqueue admin assets only on admin pages and public assets only on the front end.

The Main Plugin File

My main plugin file follows a consistent pattern that handles initialization cleanly:

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

if ( ! defined( 'ABSPATH' ) ) {
    exit; // Exit if accessed directly
}

define( 'MY_PLUGIN_VERSION', '1.0.0' );
define( 'MY_PLUGIN_PATH', plugin_dir_path( __FILE__ ) );
define( 'MY_PLUGIN_URL', plugin_dir_url( __FILE__ ) );

require_once MY_PLUGIN_PATH . 'includes/class-plugin.php';

function my_plugin_init() {
    return My_Plugin::get_instance();
}

add_action( 'plugins_loaded', 'my_plugin_init' );

The ABSPATH check prevents direct file access. The constants provide easy reference to the plugin's version, path, and URL throughout the codebase. Initializing on 'plugins_loaded' ensures other plugins have loaded first, which matters when you need to check for dependencies or integrate with other plugins.

Hooks

The Hooks System

Hooks are the foundation of WordPress plugin development. They're the mechanism that lets plugins interact with WordPress core and each other without modifying source code. Understanding hooks deeply is the single most important WordPress development skill. Everything else builds on this foundation.

WordPress provides hundreds of hooks throughout its codebase—moments where plugins can inject functionality or modify data. When you understand hooks, you can extend virtually any aspect of WordPress behavior. When you don't understand hooks, you're stuck with workarounds and hacks that break with updates.

Actions

Actions let you execute code at specific points in WordPress execution. WordPress says "this is happening now" and gives you the chance to do something in response. You're not modifying WordPress behavior—you're adding to it.

// Execute your function when WordPress initializes
add_action( 'init', 'my_plugin_register_post_type' );

function my_plugin_register_post_type() {
    register_post_type( 'book', [
        'public' => true,
        'label'  => 'Books',
    ] );
}

// Execute with priority (lower = earlier)
add_action( 'init', 'my_function', 5 );

// Accept additional arguments
add_action( 'save_post', 'my_save_function', 10, 3 );
function my_save_function( $post_id, $post, $update ) {
    // $post_id, $post object, and $update boolean available
}

Priority controls the order of execution when multiple functions hook into the same action. The default is 10; lower numbers run earlier. The third parameter to add_action specifies how many arguments your function accepts—important when hooks pass multiple values.

Filters

Filters let you modify data as it passes through WordPress. WordPress says "here's a value—want to change it before I use it?" You receive data, optionally modify it, and return the result. Always return something from a filter—failing to return breaks the chain.

// Modify the content before display
add_filter( 'the_content', 'my_plugin_add_cta' );

function my_plugin_add_cta( $content ) {
    if ( is_single() ) {
        $content .= '<div class="cta">Thanks for reading!</div>';
    }
    return $content; // Always return the value
}

// Modify with priority
add_filter( 'the_title', 'my_title_function', 20 );

The distinction between actions and filters is conceptual more than technical—they work almost identically under the hood. But the distinction matters for clarity: use actions when you're doing something, filters when you're modifying something.

Creating Custom Hooks

Good plugins provide their own hooks for extension. This lets other developers (or future you) extend your plugin without modifying its code directly. Creating hooks is simple and makes your code dramatically more flexible.

// Create an action point
do_action( 'my_plugin_before_process', $data );

// Create a filter point
$result = apply_filters( 'my_plugin_output', $default_output, $context );

The plugins I most enjoy working with are the ones that provide thoughtful hooks at logical extension points. When I need to customize their behavior, I can do it cleanly through hooks rather than hacking their code or copying and modifying functions.

Hook Naming

Prefix your hooks with your plugin name to avoid conflicts. 'my_plugin_before_save' is better than just 'before_save'. This prevents collisions with other plugins using similar names.
Security

Security Fundamentals

Security is where I see the most dangerous mistakes in WordPress plugin development. Plugins run with full WordPress privileges. A security vulnerability in your plugin is a vulnerability in every site that uses it. This isn't theoretical—plugin vulnerabilities are the most common way WordPress sites get compromised.

The good news is that security in WordPress follows predictable patterns. Sanitize inputs, escape outputs, check capabilities, verify nonces. Master these four practices and you'll avoid the vast majority of vulnerabilities. Skip any of them and you're creating attack vectors.

Sanitization

Every piece of data that comes from outside your code—user input, database queries, API responses—should be sanitized before you use it. WordPress provides functions for common data types:

// Text fields
$clean_text = sanitize_text_field( $_POST['title'] );

// Email
$clean_email = sanitize_email( $_POST['email'] );

// HTML (with allowed tags)
$clean_html = wp_kses_post( $_POST['content'] );

// Integer
$clean_int = absint( $_POST['count'] );

// URL
$clean_url = esc_url_raw( $_POST['website'] );

Match the sanitization function to the data type. Using sanitize_text_field on HTML strips the tags you might want to keep. Using wp_kses_post on a text field allows HTML you might not want. Think about what valid data looks like and choose accordingly.

Escaping

Even after sanitizing input, escape it again when outputting. Escaping prevents data from being interpreted as code—it's your defense against cross-site scripting (XSS) attacks. Different output contexts require different escaping:

// HTML output
echo esc_html( $user_input );

// Attributes
echo '<div class="' . esc_attr( $class ) . '">';

// URLs
echo '<a href="' . esc_url( $link ) . '">';

// JavaScript
echo '<script>var data = ' . wp_json_encode( $data ) . '</script>';

// Translated strings with variables
echo esc_html( sprintf( __( 'Hello %s', 'my-plugin' ), $name ) );

The "sanitize on input, escape on output" pattern means you're protected even if you make a mistake somewhere. Data gets cleaned twice—once when it enters your code, once when it leaves.

Capability Checks

Before performing any privileged action, verify the current user has permission. WordPress's capability system lets you check what the user can do:

// Check user can perform action
if ( ! current_user_can( 'edit_posts' ) ) {
    wp_die( 'Unauthorized access' );
}

// For specific post
if ( ! current_user_can( 'edit_post', $post_id ) ) {
    return;
}

Use the most specific capability that makes sense. Don't require 'manage_options' (admin-level) when 'edit_posts' (editor-level) would suffice. Overly broad capability requirements frustrate legitimate users without adding security.

Nonce Verification

Nonces (number used once) protect against cross-site request forgery (CSRF). They ensure that form submissions came from your site, not from an attacker tricking a user into clicking a malicious link:

// Create nonce field in form
wp_nonce_field( 'my_plugin_save', 'my_plugin_nonce' );

// Verify on submission
if ( ! wp_verify_nonce( $_POST['my_plugin_nonce'], 'my_plugin_save' ) ) {
    wp_die( 'Security check failed' );
}

Always verify nonces before processing form submissions or AJAX requests. The nonce system is one of WordPress's most important security features—use it consistently.

Security Is Not Optional

Every WordPress security vulnerability starts with a plugin that didn't sanitize, escape, or verify properly. Treat all user input as potentially malicious. These practices aren't paranoia—they're professional standards.
Database

Database Operations

Most plugins need to store and retrieve data. WordPress provides multiple APIs for data persistence, and choosing the right one simplifies your code while maintaining compatibility with WordPress's caching and multisite systems.

Using WordPress APIs

For most data storage needs, WordPress APIs are preferable to direct database queries. They handle caching, data validation, and multisite compatibility automatically:

// Post meta
update_post_meta( $post_id, 'my_key', $value );
$value = get_post_meta( $post_id, 'my_key', true );

// Options
update_option( 'my_plugin_settings', $settings );
$settings = get_option( 'my_plugin_settings', $defaults );

// User meta
update_user_meta( $user_id, 'preference', $value );
$pref = get_user_meta( $user_id, 'preference', true );

Post meta stores data attached to specific posts. Options store global settings. User meta stores per-user data. These three cover most plugin data needs without requiring custom tables.

Custom Queries

When WordPress APIs don't fit—complex queries, bulk operations, or custom table access—use $wpdb directly. Always use prepare() for any query involving variables to prevent SQL injection:

global $wpdb;

// Always use prepare() for variables
$results = $wpdb->get_results(
    $wpdb->prepare(
        "SELECT * FROM {$wpdb->posts} WHERE post_type = %s AND post_status = %s",
        'book',
        'publish'
    )
);

// Insert
$wpdb->insert(
    $wpdb->prefix . 'my_table',
    [ 'column' => $value ],
    [ '%s' ] // Format
);

// Update
$wpdb->update(
    $wpdb->prefix . 'my_table',
    [ 'column' => $new_value ],
    [ 'id' => $id ],
    [ '%s' ],
    [ '%d' ]
);

The prepare() method escapes values and prevents SQL injection. Never concatenate variables directly into SQL strings—that's how security vulnerabilities happen.

Creating Custom Tables

Custom tables make sense when you have data that doesn't fit WordPress's existing structures—high-volume logging, complex relational data, or performance-critical operations. Create tables during plugin activation:

function my_plugin_create_table() {
    global $wpdb;
    $table_name = $wpdb->prefix . 'my_custom_table';
    $charset_collate = $wpdb->get_charset_collate();

    $sql = "CREATE TABLE $table_name (
        id bigint(20) NOT NULL AUTO_INCREMENT,
        name varchar(255) NOT NULL,
        created_at datetime DEFAULT CURRENT_TIMESTAMP,
        PRIMARY KEY (id)
    ) $charset_collate;";

    require_once ABSPATH . 'wp-admin/includes/upgrade.php';
    dbDelta( $sql );
}

register_activation_hook( __FILE__, 'my_plugin_create_table' );

The dbDelta function handles table creation and updates intelligently—it compares your schema to what exists and makes only necessary changes. This makes plugin updates that modify database structure safer.

Admin

Admin Interfaces

Plugins often need admin interfaces for configuration and management. WordPress provides APIs for adding admin pages, registering settings, and creating the user interfaces that site administrators interact with.

Admin Menu

Add pages to the WordPress admin menu with add_menu_page for top-level items and add_submenu_page for nested items:

add_action( 'admin_menu', 'my_plugin_admin_menu' );

function my_plugin_admin_menu() {
    add_menu_page(
        'My Plugin',           // Page title
        'My Plugin',           // Menu title
        'manage_options',      // Capability
        'my-plugin',           // Slug
        'my_plugin_admin_page', // Callback
        'dashicons-admin-generic', // Icon
        30                     // Position
    );

    add_submenu_page(
        'my-plugin',           // Parent slug
        'Settings',            // Page title
        'Settings',            // Menu title
        'manage_options',
        'my-plugin-settings',
        'my_plugin_settings_page'
    );
}

Choose menu position thoughtfully. Lower numbers appear higher in the menu. Don't fight for top spots unless your plugin is the site's primary function. Most plugins should live lower in the menu or under existing sections like Settings or Tools.

Settings API

The Settings API provides a standardized way to create settings pages with proper security handling. It manages form submission, nonce verification, and data sanitization:

add_action( 'admin_init', 'my_plugin_register_settings' );

function my_plugin_register_settings() {
    register_setting( 'my_plugin_options', 'my_plugin_settings' );

    add_settings_section(
        'my_plugin_main',
        'Main Settings',
        'my_plugin_section_callback',
        'my-plugin'
    );

    add_settings_field(
        'api_key',
        'API Key',
        'my_plugin_api_field_callback',
        'my-plugin',
        'my_plugin_main'
    );
}

function my_plugin_api_field_callback() {
    $options = get_option( 'my_plugin_settings' );
    echo '<input type="text" name="my_plugin_settings[api_key]" value="' .
         esc_attr( $options['api_key'] ?? '' ) . '">';
}

The Settings API has a learning curve, but it handles security correctly and creates consistent UI. For simple settings pages, it's the right choice. For complex interfaces, you might build custom solutions—but you'll need to handle security yourself.

BestPractices

Best Practices

Beyond the fundamentals, certain practices distinguish professional plugin development from amateur work. These aren't just coding preferences—they affect how maintainable your plugin is over time and how well it plays with others in the WordPress ecosystem.

Coding Standards

WordPress has documented coding standards for PHP, JavaScript, CSS, and HTML. Following them makes your code more readable to other WordPress developers and easier to maintain. Use tools like PHPCS with the WordPress coding standard ruleset to catch issues automatically. Meaningful function and variable names matter more than clever shortcuts. Comments should explain why, not what—the code shows what happens; comments explain the reasoning.

Internationalization

Even if you only speak English, internationalize your plugins. Translation functions wrap your strings so translators can provide localized versions. It's much harder to add internationalization later than to build it in from the start:

// Text domain in plugin header
// Text Domain: my-plugin

// Translatable strings
__( 'Settings saved.', 'my-plugin' );
_e( 'Click here', 'my-plugin' );
esc_html__( 'Welcome', 'my-plugin' );

// With placeholders
sprintf( __( 'Hello %s', 'my-plugin' ), $name );

The text domain connects strings to translation files. Use it consistently throughout your plugin. WordPress's translation community can then contribute translations without touching your code.

Activation and Deactivation

Plugins can run code when activated or deactivated. Use activation for setup tasks—creating database tables, setting default options, flushing rewrite rules. Use deactivation for lightweight cleanup—clearing scheduled events, but not deleting user data:

register_activation_hook( __FILE__, 'my_plugin_activate' );
register_deactivation_hook( __FILE__, 'my_plugin_deactivate' );

function my_plugin_activate() {
    // Create tables, set defaults, flush rewrite rules
    flush_rewrite_rules();
}

function my_plugin_deactivate() {
    // Clean up temporary data
    flush_rewrite_rules();
}

Uninstall Cleanup

When users delete your plugin, clean up after yourself. The uninstall.php file runs when the plugin is deleted (not just deactivated), and it's where you should remove database tables, options, and other persistent data:

// uninstall.php
if ( ! defined( 'WP_UNINSTALL_PLUGIN' ) ) {
    exit;
}

// Remove options
delete_option( 'my_plugin_settings' );

// Remove custom tables
global $wpdb;
$wpdb->query( "DROP TABLE IF EXISTS {$wpdb->prefix}my_custom_table" );

Good uninstall behavior is professional courtesy. Users who remove your plugin shouldn't have leftover data cluttering their database.

Do This

Prefix everything. Use WordPress APIs when available. Sanitize input, escape output. Check capabilities. Write tests. Document your code.

Avoid This

Direct database queries without prepare(). Echoing unsanitized input. Hardcoded paths. Global variables everywhere. Ignoring WordPress coding standards.

Conclusion

Next Steps

Plugin development is a deep topic—this article covers fundamentals, but there's much more to explore. The official Plugin Developer Handbook on developer.wordpress.org goes into detail on every topic I've touched here. Studying well-written plugins in the WordPress.org repository shows you patterns in practice. Building small, focused plugins gives you hands-on experience without overwhelming complexity.

As you grow, explore object-oriented approaches for larger plugins—classes provide better organization and encapsulation for complex functionality. The REST API enables modern integrations and opens possibilities for headless and decoupled architectures. Custom blocks extend the block editor with functionality tailored to your needs.

The fundamentals covered here—hooks, security, database operations, admin interfaces—form the foundation for any WordPress plugin. Master these patterns and you can build virtually anything WordPress needs. The specific APIs and tools will evolve, but the underlying principles persist. A solid understanding of WordPress architecture serves you whether you're building a simple utility plugin or a complex enterprise solution.

Frequently Asked Questions

When should I build a custom plugin vs. use an existing one?

Build custom when existing plugins don't fit your needs, add unnecessary bloat, or create security/maintenance concerns. Use existing plugins when they solve your problem well and come from reputable developers. Custom isn't always better—it's additional code to maintain.

Can I sell plugins I create?

Yes, as long as they're GPL-compatible (WordPress's license requires this for derivative works). You can sell plugins directly, through marketplaces like CodeCanyon, or through subscription models. The plugin itself must be GPL, but you can charge for support, updates, and access.

How do I update my plugin safely?

Use semantic versioning. Test thoroughly before release. Provide upgrade routines for database changes. Announce breaking changes in advance. Consider backwards compatibility for users who delay updates.

Should I host my plugin on WordPress.org?

For free plugins, yes—it provides distribution, automatic updates, and credibility. For premium plugins, you'll need your own distribution. WordPress.org has guidelines; not all plugins qualify.
WordPress Plugin Development PHP Web Development Custom Development
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

Object-Oriented PHP for WordPress Developers

14 min read
WordPress Enterprise

Building Custom WordPress Plugins: When and Why

9 min read

Need a custom WordPress plugin built?

I develop custom WordPress plugins that solve specific business problems. Let's discuss your requirements and build something that fits your needs perfectly.

© 2026 williamalexander.co. All rights reserved.