🔌 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.
📋 Table of Contents
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
/**
* 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
- Go to WordPress admin dashboard
- Navigate to Plugins → Installed Plugins
- Find "My First Plugin" in the list
- Click "Activate"
- 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.
/**
* 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
/**
* 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.
/**
* 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
/**
* 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
/**
* 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
/**
* 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
/**
* 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.
/**
* 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
/**
* 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:
// 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
/**
* 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
/**
* 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
/**
* 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
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
- Practice by building simple plugins first
- Study popular plugins' source code
- Read WordPress Codex and Developer Handbook
- Join WordPress development communities
- 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
- WordPress Developer Resources
- Plugin Developer Handbook
- WordPress Coding Standards
- WordPress Code Reference
- Plugin Submission Guidelines
🎉 Congratulations! You now have the knowledge to build powerful WordPress plugins. Start small, keep learning, and happy coding!