← Back to Blog
WordPress Development

How to Build a Custom WordPress Plugin from Scratch

April 30, 2026 205 views Amanur Rahman
Learn the fundamentals of WordPress plugin development including hooks, filters, database tables, and admin interfaces.

🔌 How to Build a Custom WordPress Plugin from Scratch

📌 What You'll Learn: Complete step-by-step guide to creating your first WordPress plugin from the ground up. We'll cover everything from basic plugin structure to advanced features like custom post types, meta boxes, settings pages, and more.

Introduction to WordPress Plugins

WordPress plugins are PHP-based extensions that add specific features and functionality to your WordPress website. Unlike themes that control appearance, plugins extend functionality without altering the core WordPress files.

✅ Why Build Custom Plugins?

  • Modularity: Keep custom functionality separate from your theme
  • Reusability: Use the same plugin across multiple sites
  • Updates: Theme updates won't affect your custom code
  • Distribution: Share or sell your plugin to others
  • Learning: Deep understanding of WordPress architecture

Prerequisites & Requirements

What You Need to Know

Skill Level Description
PHP Required Basic to intermediate PHP knowledge
WordPress Required Understanding of WordPress hooks and filters
HTML/CSS Helpful For creating frontend interfaces
JavaScript Optional For interactive features
MySQL Optional For custom database tables

Development Environment

  • Local WordPress installation (XAMPP, WAMP, Local by Flywheel, or MAMP)
  • Code editor (VS Code, Sublime Text, or PHPStorm)
  • FTP client (FileZilla) - optional
  • Browser developer tools

Plugin File Structure

Understanding the basic plugin structure is crucial before diving into development.

Minimum Required Files

my-custom-plugin/
│
├── my-custom-plugin.php    (Main plugin file)
└── readme.txt              (Plugin documentation)

Professional Plugin Structure

my-custom-plugin/
│
├── admin/                   (Admin-specific functionality)
│   ├── css/
│   ├── js/
│   └── partials/
│
├── includes/                (Core plugin files)
│   ├── class-activator.php
│   ├── class-deactivator.php
│   └── class-loader.php
│
├── public/                  (Public-facing functionality)
│   ├── css/
│   ├── js/
│   └── partials/
│
├── languages/               (Translation files)
├── assets/                  (Images, icons)
│
├── my-custom-plugin.php    (Main plugin file)
├── uninstall.php           (Cleanup on uninstall)
└── readme.txt              (Documentation)

Creating Your First Basic Plugin

1 Create the Plugin Folder

Navigate to wp-content/plugins/ and create a new folder called my-first-plugin

⚠️ Naming Convention: Use lowercase letters and hyphens only. No spaces or special characters.

2 Create the Main Plugin File

Inside the plugin folder, create my-first-plugin.php with this header:

PHP - my-first-plugin.php
<?php
/**
 * Plugin Name: My First Plugin
 * Plugin URI: https://yourwebsite.com/my-first-plugin
 * Description: A simple custom WordPress plugin to demonstrate plugin development.
 * Version: 1.0.0
 * Author: Your Name
 * Author URI: https://yourwebsite.com
 * License: GPL v2 or later
 * License URI: https://www.gnu.org/licenses/gpl-2.0.html
 * Text Domain: my-first-plugin
 * Domain Path: /languages
 */

// If this file is called directly, abort.
if ( ! defined( 'WPINC' ) ) {
    die;
}

// Plugin version
define( 'MY_PLUGIN_VERSION', '1.0.0' );

/**
 * Plugin activation hook
 */
function mfp_activate() {
    // Code to run on activation
    flush_rewrite_rules();
}
register_activation_hook( __FILE__, 'mfp_activate' );

/**
 * Plugin deactivation hook
 */
function mfp_deactivate() {
    // Code to run on deactivation
    flush_rewrite_rules();
}
register_deactivation_hook( __FILE__, 'mfp_deactivate' );

/**
 * Main plugin functionality
 */
function mfp_init() {
    // Your plugin code goes here
    add_action( 'wp_footer', 'mfp_display_message' );
}
add_action( 'plugins_loaded', 'mfp_init' );

/**
 * Display a simple message in the footer
 */
function mfp_display_message() {
    echo '<div style="text-align:center; padding: 20px; background: #f0f0f0;">';
    echo '<p>Hello from My First Plugin!</p>';
    echo '</div>';
}
?>

Header Information Explained

  • Plugin Name: Display name in WordPress admin (Required)
  • Description: Brief description of what plugin does (Required)
  • Version: Current version number (Required)
  • Author: Your name or company
  • License: Licensing information (GPL recommended)
  • Text Domain: For translations

3 Activate Your Plugin

  1. Go to WordPress admin dashboard
  2. Navigate to Plugins → Installed Plugins
  3. Find "My First Plugin" in the list
  4. Click "Activate"
  5. Visit your website frontend to see the message!

🎉 Congratulations! You've just created and activated your first WordPress plugin!

Advanced Plugin Features

Creating a Custom Post Type

Custom Post Types allow you to create content types beyond standard posts and pages.

PHP - Custom Post Type
/**
 * Register Custom Post Type
 */
function mfp_register_book_post_type() {
    $labels = array(
        'name'               => 'Books',
        'singular_name'      => 'Book',
        'menu_name'          => 'Books',
        'add_new'            => 'Add New',
        'add_new_item'       => 'Add New Book',
        'edit_item'          => 'Edit Book',
        'new_item'           => 'New Book',
        'view_item'          => 'View Book',
        'search_items'       => 'Search Books',
        'not_found'          => 'No books found',
        'not_found_in_trash' => 'No books found in Trash',
    );

    $args = array(
        'labels'             => $labels,
        'public'             => true,
        'publicly_queryable' => true,
        'show_ui'            => true,
        'show_in_menu'       => true,
        'query_var'          => true,
        'rewrite'            => array( 'slug' => 'book' ),
        'capability_type'    => 'post',
        'has_archive'        => true,
        'hierarchical'       => false,
        'menu_position'      => 5,
        'menu_icon'          => 'dashicons-book',
        'supports'           => array( 'title', 'editor', 'thumbnail', 'excerpt' ),
    );

    register_post_type( 'book', $args );
}
add_action( 'init', 'mfp_register_book_post_type' );

Adding Custom Taxonomies

PHP - Custom Taxonomy
/**
 * Register Custom Taxonomy
 */
function mfp_register_book_genre_taxonomy() {
    $labels = array(
        'name'              => 'Genres',
        'singular_name'     => 'Genre',
        'search_items'      => 'Search Genres',
        'all_items'         => 'All Genres',
        'parent_item'       => 'Parent Genre',
        'parent_item_colon' => 'Parent Genre:',
        'edit_item'         => 'Edit Genre',
        'update_item'       => 'Update Genre',
        'add_new_item'      => 'Add New Genre',
        'new_item_name'     => 'New Genre Name',
        'menu_name'         => 'Genres',
    );

    $args = array(
        'hierarchical'      => true,
        'labels'            => $labels,
        'show_ui'           => true,
        'show_admin_column' => true,
        'query_var'         => true,
        'rewrite'           => array( 'slug' => 'genre' ),
    );

    register_taxonomy( 'genre', array( 'book' ), $args );
}
add_action( 'init', 'mfp_register_book_genre_taxonomy' );

Creating Meta Boxes

Meta boxes allow you to add custom fields to your post edit screens.

PHP - Meta Box
/**
 * Add Meta Box
 */
function mfp_add_book_meta_box() {
    add_meta_box(
        'mfp_book_details',           // Unique ID
        'Book Details',               // Box title
        'mfp_book_meta_box_callback', // Content callback
        'book',                       // Post type
        'normal',                     // Context
        'high'                        // Priority
    );
}
add_action( 'add_meta_boxes', 'mfp_add_book_meta_box' );

/**
 * Meta Box Callback Function
 */
function mfp_book_meta_box_callback( $post ) {
    // Add nonce for security
    wp_nonce_field( 'mfp_save_book_meta', 'mfp_book_meta_nonce' );

    // Retrieve existing values
    $author = get_post_meta( $post->ID, '_book_author', true );
    $isbn = get_post_meta( $post->ID, '_book_isbn', true );
    $pages = get_post_meta( $post->ID, '_book_pages', true );

    // Output fields
    ?>
    <p>
        <label for="book_author">Author:</label>
        <input type="text" id="book_author" name="book_author" 
               value="<?php echo esc_attr( $author ); ?>" style="width: 100%;">
    </p>
    <p>
        <label for="book_isbn">ISBN:</label>
        <input type="text" id="book_isbn" name="book_isbn" 
               value="<?php echo esc_attr( $isbn ); ?>" style="width: 100%;">
    </p>
    <p>
        <label for="book_pages">Number of Pages:</label>
        <input type="number" id="book_pages" name="book_pages" 
               value="<?php echo esc_attr( $pages ); ?>" style="width: 100%;">
    </p>
    <?php
}

/**
 * Save Meta Box Data
 */
function mfp_save_book_meta( $post_id ) {
    // Check nonce
    if ( ! isset( $_POST['mfp_book_meta_nonce'] ) || 
         ! wp_verify_nonce( $_POST['mfp_book_meta_nonce'], 'mfp_save_book_meta' ) ) {
        return;
    }

    // Check autosave
    if ( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE ) {
        return;
    }

    // Check permissions
    if ( ! current_user_can( 'edit_post', $post_id ) ) {
        return;
    }

    // Save data
    if ( isset( $_POST['book_author'] ) ) {
        update_post_meta( $post_id, '_book_author', sanitize_text_field( $_POST['book_author'] ) );
    }

    if ( isset( $_POST['book_isbn'] ) ) {
        update_post_meta( $post_id, '_book_isbn', sanitize_text_field( $_POST['book_isbn'] ) );
    }

    if ( isset( $_POST['book_pages'] ) ) {
        update_post_meta( $post_id, '_book_pages', absint( $_POST['book_pages'] ) );
    }
}
add_action( 'save_post_book', 'mfp_save_book_meta' );

Working with Database

Creating Custom Database Tables

PHP - Custom Table Creation
/**
 * Create custom table on plugin activation
 */
function mfp_create_custom_table() {
    global $wpdb;

    $table_name = $wpdb->prefix . 'book_reviews';
    $charset_collate = $wpdb->get_charset_collate();

    $sql = "CREATE TABLE $table_name (
        id mediumint(9) NOT NULL AUTO_INCREMENT,
        book_id bigint(20) NOT NULL,
        reviewer_name varchar(100) NOT NULL,
        review_text text NOT NULL,
        rating tinyint(1) NOT NULL,
        created_at datetime DEFAULT CURRENT_TIMESTAMP NOT NULL,
        PRIMARY KEY  (id),
        KEY book_id (book_id)
    ) $charset_collate;";

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

    add_option( 'mfp_db_version', '1.0' );
}
register_activation_hook( __FILE__, 'mfp_create_custom_table' );

Inserting Data

PHP - Insert Data
/**
 * Insert review into database
 */
function mfp_insert_review( $book_id, $reviewer_name, $review_text, $rating ) {
    global $wpdb;
    
    $table_name = $wpdb->prefix . 'book_reviews';
    
    $result = $wpdb->insert(
        $table_name,
        array(
            'book_id'       => $book_id,
            'reviewer_name' => $reviewer_name,
            'review_text'   => $review_text,
            'rating'        => $rating,
        ),
        array(
            '%d',  // book_id
            '%s',  // reviewer_name
            '%s',  // review_text
            '%d',  // rating
        )
    );
    
    return $result;
}

Querying Data

PHP - Query Data
/**
 * Get reviews for a specific book
 */
function mfp_get_book_reviews( $book_id ) {
    global $wpdb;
    
    $table_name = $wpdb->prefix . 'book_reviews';
    
    $reviews = $wpdb->get_results(
        $wpdb->prepare(
            "SELECT * FROM $table_name WHERE book_id = %d ORDER BY created_at DESC",
            $book_id
        )
    );
    
    return $reviews;
}

Creating Admin Settings Page

PHP - Settings Page
/**
 * Add settings page to admin menu
 */
function mfp_add_settings_page() {
    add_options_page(
        'My Plugin Settings',          // Page title
        'My Plugin',                   // Menu title
        'manage_options',              // Capability
        'my-plugin-settings',          // Menu slug
        'mfp_settings_page_callback'   // Callback function
    );
}
add_action( 'admin_menu', 'mfp_add_settings_page' );

/**
 * Settings page HTML
 */
function mfp_settings_page_callback() {
    ?>
    <div class="wrap">
        <h1>My Plugin Settings</h1>
        <form method="post" action="options.php">
            <?php
                settings_fields( 'mfp_settings_group' );
                do_settings_sections( 'my-plugin-settings' );
                submit_button();
            ?>
        </form>
    </div>
    <?php
}

/**
 * Register settings
 */
function mfp_register_settings() {
    // Register setting
    register_setting(
        'mfp_settings_group',
        'mfp_options',
        'mfp_sanitize_options'
    );

    // Add settings section
    add_settings_section(
        'mfp_main_section',
        'Main Settings',
        'mfp_main_section_callback',
        'my-plugin-settings'
    );

    // Add settings field
    add_settings_field(
        'mfp_enable_feature',
        'Enable Feature',
        'mfp_enable_feature_callback',
        'my-plugin-settings',
        'mfp_main_section'
    );
}
add_action( 'admin_init', 'mfp_register_settings' );

/**
 * Section callback
 */
function mfp_main_section_callback() {
    echo '<p>Configure your plugin settings below:</p>';
}

/**
 * Field callback
 */
function mfp_enable_feature_callback() {
    $options = get_option( 'mfp_options' );
    $checked = isset( $options['enable_feature'] ) ? checked( $options['enable_feature'], 1, false ) : '';
    ?>
    <input type="checkbox" id="enable_feature" name="mfp_options[enable_feature]" value="1" <?php echo $checked; ?> />
    <label for="enable_feature">Enable this awesome feature</label>
    <?php
}

/**
 * Sanitize options
 */
function mfp_sanitize_options( $input ) {
    $sanitized = array();
    
    if ( isset( $input['enable_feature'] ) ) {
        $sanitized['enable_feature'] = 1;
    }
    
    return $sanitized;
}

Adding Shortcodes

Shortcodes allow users to easily insert plugin functionality into posts and pages.

PHP - Simple Shortcode
/**
 * Simple shortcode example
 * Usage: [display_books]
 */
function mfp_display_books_shortcode( $atts ) {
    // Parse attributes
    $atts = shortcode_atts( array(
        'limit' => 5,
        'genre' => '',
    ), $atts );

    // Query books
    $args = array(
        'post_type'      => 'book',
        'posts_per_page' => intval( $atts['limit'] ),
    );

    if ( ! empty( $atts['genre'] ) ) {
        $args['tax_query'] = array(
            array(
                'taxonomy' => 'genre',
                'field'    => 'slug',
                'terms'    => sanitize_text_field( $atts['genre'] ),
            ),
        );
    }

    $books = new WP_Query( $args );

    // Build output
    $output = '<div class="books-list">';

    if ( $books->have_posts() ) {
        while ( $books->have_posts() ) {
            $books->the_post();
            $author = get_post_meta( get_the_ID(), '_book_author', true );
            
            $output .= '<div class="book-item">';
            $output .= '<h3>' . get_the_title() . '</h3>';
            $output .= '<p><strong>Author:</strong> ' . esc_html( $author ) . '</p>';
            $output .= '<div>' . get_the_excerpt() . '</div>';
            $output .= '</div>';
        }
        wp_reset_postdata();
    } else {
        $output .= '<p>No books found.</p>';
    }

    $output .= '</div>';

    return $output;
}
add_shortcode( 'display_books', 'mfp_display_books_shortcode' );

💡 Shortcode Usage Examples:

  • [display_books] - Display 5 books (default)
  • [display_books limit="10"] - Display 10 books
  • [display_books genre="fiction"] - Display fiction books only
  • [display_books limit="3" genre="mystery"] - Display 3 mystery books

Security Best Practices

🔒 Critical Security Rules:

  • Never trust user input - always validate and sanitize
  • Use nonces for form submissions
  • Check user capabilities before allowing actions
  • Escape output to prevent XSS attacks
  • Use prepared statements for database queries

Data Sanitization Functions

Function Use Case
sanitize_text_field() Single line text input
sanitize_textarea_field() Multi-line text input
sanitize_email() Email addresses
sanitize_url() URLs
absint() Positive integers
sanitize_key() Lowercase alphanumeric keys

Output Escaping Functions

Function Use Case
esc_html() HTML content
esc_attr() HTML attributes
esc_url() URLs
esc_js() JavaScript strings
wp_kses_post() Allow some HTML tags

Security Example

PHP - Secure Form Processing
/**
 * Process form submission securely
 */
function mfp_process_form() {
    // Verify nonce
    if ( ! isset( $_POST['mfp_nonce'] ) || 
         ! wp_verify_nonce( $_POST['mfp_nonce'], 'mfp_form_action' ) ) {
        wp_die( 'Security check failed!' );
    }

    // Check user capability
    if ( ! current_user_can( 'edit_posts' ) ) {
        wp_die( 'You do not have permission to perform this action.' );
    }

    // Sanitize input
    $name = isset( $_POST['name'] ) ? sanitize_text_field( $_POST['name'] ) : '';
    $email = isset( $_POST['email'] ) ? sanitize_email( $_POST['email'] ) : '';
    $message = isset( $_POST['message'] ) ? sanitize_textarea_field( $_POST['message'] ) : '';

    // Validate
    if ( empty( $name ) || empty( $email ) || ! is_email( $email ) ) {
        wp_die( 'Please fill in all required fields correctly.' );
    }

    // Process data safely
    global $wpdb;
    $table_name = $wpdb->prefix . 'contact_forms';
    
    $wpdb->insert(
        $table_name,
        array(
            'name'    => $name,
            'email'   => $email,
            'message' => $message,
        ),
        array( '%s', '%s', '%s' )
    );

    // Redirect
    wp_safe_redirect( add_query_arg( 'success', '1', wp_get_referer() ) );
    exit;
}

Testing & Debugging

Enable WordPress Debug Mode

Add these lines to your wp-config.php file:

PHP - wp-config.php
// Enable WP_DEBUG mode
define( 'WP_DEBUG', true );

// Enable Debug logging to the /wp-content/debug.log file
define( 'WP_DEBUG_LOG', true );

// Disable display of errors and warnings
define( 'WP_DEBUG_DISPLAY', false );
@ini_set( 'display_errors', 0 );

// Use dev versions of core JS and CSS files
define( 'SCRIPT_DEBUG', true );

Debugging Functions

PHP - Debug Helpers
/**
 * Debug helper functions
 */

// Write to debug log
function mfp_log( $message ) {
    if ( WP_DEBUG === true ) {
        if ( is_array( $message ) || is_object( $message ) ) {
            error_log( print_r( $message, true ) );
        } else {
            error_log( $message );
        }
    }
}

// Display variable content (for development only)
function mfp_debug_print( $var ) {
    if ( WP_DEBUG === true && current_user_can( 'manage_options' ) ) {
        echo '<pre>';
        print_r( $var );
        echo '</pre>';
    }
}

// Usage examples:
mfp_log( 'Plugin initialized' );
mfp_log( $my_array );
mfp_debug_print( $query_results );

Testing Checklist

✅ Before Publishing Your Plugin:

  • Test with different WordPress versions
  • Test with different PHP versions (minimum 7.4+)
  • Test on fresh WordPress installation
  • Test with different themes
  • Test with other popular plugins activated
  • Check for JavaScript errors in console
  • Validate HTML output
  • Test activation/deactivation
  • Test uninstall process
  • Check database queries for performance
  • Run security scans
  • Test on mobile devices

Plugin Development Best Practices

🎯 Code Organization

  • Use meaningful function and variable names with prefixes
  • Follow WordPress Coding Standards
  • Comment your code thoroughly
  • Keep functions small and focused
  • Use classes for better organization in larger plugins

⚡ Performance

  • Only load scripts/styles when needed
  • Use transients for caching expensive operations
  • Minimize database queries
  • Use WordPress built-in functions when possible
  • Avoid querying in loops

🌍 Internationalization

  • Make your plugin translation-ready
  • Use text domains consistently
  • Wrap all strings in translation functions
  • Provide .pot file for translators

Enqueuing Scripts and Styles Properly

PHP - Enqueue Assets
/**
 * Enqueue plugin styles and scripts
 */
function mfp_enqueue_assets() {
    // Enqueue CSS
    wp_enqueue_style(
        'mfp-style',
        plugin_dir_url( __FILE__ ) . 'css/style.css',
        array(),
        MY_PLUGIN_VERSION
    );

    // Enqueue JavaScript
    wp_enqueue_script(
        'mfp-script',
        plugin_dir_url( __FILE__ ) . 'js/script.js',
        array( 'jquery' ),
        MY_PLUGIN_VERSION,
        true
    );

    // Pass data to JavaScript
    wp_localize_script(
        'mfp-script',
        'mfpData',
        array(
            'ajaxUrl' => admin_url( 'admin-ajax.php' ),
            'nonce'   => wp_create_nonce( 'mfp-nonce' ),
        )
    );
}
add_action( 'wp_enqueue_scripts', 'mfp_enqueue_assets' );

/**
 * Enqueue admin assets
 */
function mfp_enqueue_admin_assets( $hook ) {
    // Only load on specific admin pages
    if ( 'settings_page_my-plugin-settings' !== $hook ) {
        return;
    }

    wp_enqueue_style(
        'mfp-admin-style',
        plugin_dir_url( __FILE__ ) . 'admin/css/admin.css',
        array(),
        MY_PLUGIN_VERSION
    );

    wp_enqueue_script(
        'mfp-admin-script',
        plugin_dir_url( __FILE__ ) . 'admin/js/admin.js',
        array( 'jquery' ),
        MY_PLUGIN_VERSION,
        true
    );
}
add_action( 'admin_enqueue_scripts', 'mfp_enqueue_admin_assets' );

AJAX Implementation

PHP - AJAX Handler
/**
 * AJAX handler for logged-in users
 */
function mfp_ajax_handler() {
    // Check nonce
    check_ajax_referer( 'mfp-nonce', 'nonce' );

    // Get and sanitize input
    $data = isset( $_POST['data'] ) ? sanitize_text_field( $_POST['data'] ) : '';

    // Process request
    $result = array(
        'success' => true,
        'message' => 'Data processed successfully',
        'data'    => $data,
    );

    // Return JSON response
    wp_send_json_success( $result );
}
add_action( 'wp_ajax_mfp_action', 'mfp_ajax_handler' );
add_action( 'wp_ajax_nopriv_mfp_action', 'mfp_ajax_handler' ); // For non-logged-in users
JavaScript - AJAX Request
jQuery(document).ready(function($) {
    $('#submit-button').on('click', function(e) {
        e.preventDefault();

        $.ajax({
            url: mfpData.ajaxUrl,
            type: 'POST',
            data: {
                action: 'mfp_action',
                nonce: mfpData.nonce,
                data: $('#input-field').val()
            },
            beforeSend: function() {
                $('#submit-button').prop('disabled', true);
            },
            success: function(response) {
                if (response.success) {
                    console.log('Success:', response.data);
                    alert(response.data.message);
                }
            },
            error: function(xhr, status, error) {
                console.error('Error:', error);
                alert('An error occurred. Please try again.');
            },
            complete: function() {
                $('#submit-button').prop('disabled', false);
            }
        });
    });
});

Conclusion

You've now learned the fundamentals of WordPress plugin development from scratch! This guide covered:

📚 What We Covered:

  • ✅ Plugin structure and file organization
  • ✅ Creating custom post types and taxonomies
  • ✅ Working with meta boxes and custom fields
  • ✅ Database operations and custom tables
  • ✅ Admin settings pages
  • ✅ Shortcodes and AJAX
  • ✅ Security best practices
  • ✅ Testing and debugging techniques

Next Steps

  1. Practice by building simple plugins first
  2. Study popular plugins' source code
  3. Read WordPress Codex and Developer Handbook
  4. Join WordPress development communities
  5. Consider submitting your plugin to WordPress.org repository

⚠️ Remember: Always test your plugins thoroughly in a development environment before deploying to production. Keep security, performance, and user experience as top priorities.

Useful Resources

🎉 Congratulations! You now have the knowledge to build powerful WordPress plugins. Start small, keep learning, and happy coding!

Tags: WordPress Plugin Development PHP Tutorial

Need Help with Your WordPress Project?

Let's discuss how I can help you build something amazing!

Get in Touch →
← Back to Blog