Azwar's Blog
WordPress Plugin Development: Creating Custom Functionality from Scratch
Hi everyone! I'm Azwar, a WordPress developer with extensive experience in plugin development. In this comprehensive guide, I'll walk you through the process of creating custom WordPress plugins from scratch, sharing the techniques and best practices I've learned over three years of WordPress development.

WordPress plugins are powerful tools that extend the functionality of WordPress websites. Whether you're creating plugins for clients or building your own solutions, understanding plugin development opens up endless possibilities for customization and automation.
Understanding WordPress Plugin Structure
Every WordPress plugin follows a specific structure. Here's the essential file hierarchy:
my-custom-plugin/
├── my-custom-plugin.php
├── includes/
│ ├── class-plugin-main.php
│ ├── class-admin.php
│ └── class-public.php
├── admin/
│ ├── css/
│ ├── js/
│ └── partials/
├── public/
│ ├── css/
│ ├── js/
│ └── partials/
├── languages/
└── readme.txt
Step 1: Creating the Main Plugin File
The main plugin file contains the plugin header and initialization code:
<?php /** * Plugin Name: My Custom Plugin * Plugin URI: https://yourwebsite.com/my-custom-plugin * Description: A custom WordPress plugin that adds amazing functionality to your website. * Version: 1.0.0 * Author: Azwar * Author URI: https://yourwebsite.com * License: GPL v2 or later * License URI: https://www.gnu.org/licenses/gpl-2.0.html * Text Domain: my-custom-plugin * Domain Path: /languages */ // Prevent direct access if (!defined('ABSPATH')) { exit; } // Define plugin constants define('MCP_PLUGIN_URL', plugin_dir_url(__FILE__)); define('MCP_PLUGIN_PATH', plugin_dir_path(__FILE__)); define('MCP_PLUGIN_VERSION', '1.0.0'); // Include the main plugin class require_once MCP_PLUGIN_PATH . 'includes/class-plugin-main.php'; // Initialize the plugin function run_my_custom_plugin() { $plugin = new My_Custom_Plugin_Main(); $plugin->run(); } run_my_custom_plugin();
Step 2: Creating the Main Plugin Class
Create the main plugin class that handles initialization:
<?php // includes/class-plugin-main.php class My_Custom_Plugin_Main { protected $loader; protected $plugin_name; protected $version; public function __construct() { $this->version = MCP_PLUGIN_VERSION; $this->plugin_name = 'my-custom-plugin'; $this->load_dependencies(); $this->set_locale(); $this->define_admin_hooks(); $this->define_public_hooks(); } private function load_dependencies() { require_once MCP_PLUGIN_PATH . 'includes/class-loader.php'; require_once MCP_PLUGIN_PATH . 'includes/class-admin.php'; require_once MCP_PLUGIN_PATH . 'includes/class-public.php'; $this->loader = new My_Custom_Plugin_Loader(); } private function set_locale() { load_plugin_textdomain( 'my-custom-plugin', false, dirname(dirname(plugin_basename(__FILE__))) . '/languages/' ); } private function define_admin_hooks() { $plugin_admin = new My_Custom_Plugin_Admin($this->get_plugin_name(), $this->get_version()); $this->loader->add_action('admin_enqueue_scripts', $plugin_admin, 'enqueue_styles'); $this->loader->add_action('admin_enqueue_scripts', $plugin_admin, 'enqueue_scripts'); $this->loader->add_action('admin_menu', $plugin_admin, 'add_plugin_admin_menu'); } private function define_public_hooks() { $plugin_public = new My_Custom_Plugin_Public($this->get_plugin_name(), $this->get_version()); $this->loader->add_action('wp_enqueue_scripts', $plugin_public, 'enqueue_styles'); $this->loader->add_action('wp_enqueue_scripts', $plugin_public, 'enqueue_scripts'); } public function run() { $this->loader->run(); } public function get_plugin_name() { return $this->plugin_name; } public function get_version() { return $this->version; } }
Step 3: Creating a Custom Post Type
Many plugins need custom post types. Here's how to create one:
// includes/class-admin.php class My_Custom_Plugin_Admin { public function __construct($plugin_name, $version) { $this->plugin_name = $plugin_name; $this->version = $version; add_action('init', array($this, 'register_custom_post_type')); add_action('add_meta_boxes', array($this, 'add_custom_meta_boxes')); add_action('save_post', array($this, 'save_custom_meta')); } public function register_custom_post_type() { $labels = array( 'name' => 'Custom Items', 'singular_name' => 'Custom Item', 'menu_name' => 'Custom Items', 'add_new' => 'Add New', 'add_new_item' => 'Add New Custom Item', 'edit_item' => 'Edit Custom Item', 'new_item' => 'New Custom Item', 'view_item' => 'View Custom Item', 'search_items' => 'Search Custom Items', 'not_found' => 'No custom items found', 'not_found_in_trash' => 'No custom items found in trash' ); $args = array( 'labels' => $labels, 'public' => true, 'has_archive' => true, 'publicly_queryable' => true, 'show_ui' => true, 'show_in_menu' => true, 'query_var' => true, 'rewrite' => array('slug' => 'custom-items'), 'capability_type' => 'post', 'supports' => array('title', 'editor', 'thumbnail', 'excerpt'), 'menu_icon' => 'dashicons-admin-generic' ); register_post_type('custom_item', $args); } public function add_custom_meta_boxes() { add_meta_box( 'custom_item_details', 'Custom Item Details', array($this, 'custom_meta_box_callback'), 'custom_item', 'normal', 'high' ); } public function custom_meta_box_callback($post) { wp_nonce_field('save_custom_meta', 'custom_meta_nonce'); $custom_field = get_post_meta($post->ID, '_custom_field', true); ?> <table class="form-table"> <tr> <th><label for="custom_field">Custom Field</label></th> <td> <input type="text" id="custom_field" name="custom_field" value="<?php echo esc_attr($custom_field); ?>" class="regular-text" /> <p class="description">Enter a custom value for this item.</p> </td> </tr> </table> <?php } public function save_custom_meta($post_id) { if (!isset($_POST['custom_meta_nonce']) || !wp_verify_nonce($_POST['custom_meta_nonce'], 'save_custom_meta')) { return; } if (defined('DOING_AUTOSAVE') && DOING_AUTOSAVE) { return; } if (!current_user_can('edit_post', $post_id)) { return; } if (isset($_POST['custom_field'])) { update_post_meta($post_id, '_custom_field', sanitize_text_field($_POST['custom_field'])); } } }
Step 4: Creating Shortcodes
Shortcodes allow users to add plugin functionality to posts and pages:
// includes/class-public.php class My_Custom_Plugin_Public { public function __construct($plugin_name, $version) { $this->plugin_name = $plugin_name; $this->version = $version; add_shortcode('custom_shortcode', array($this, 'custom_shortcode_callback')); } public function custom_shortcode_callback($atts) { $atts = shortcode_atts(array( 'id' => 0, 'title' => 'Default Title' ), $atts); $output = '<div class="custom-shortcode">'; $output .= '<h3>' . esc_html($atts['title']) . '</h3>'; if ($atts['id']) { $post = get_post($atts['id']); if ($post) { $output .= '<p>' . esc_html($post->post_content) . '</p>'; } } $output .= '</div>'; return $output; } }
Step 5: Creating Admin Settings Page
Most plugins need an admin settings page:
// Add this to class-admin.php public function add_plugin_admin_menu() { add_menu_page( 'My Custom Plugin Settings', 'Custom Plugin', 'manage_options', 'my-custom-plugin', array($this, 'display_plugin_admin_page'), 'dashicons-admin-generic', 20 ); } public function display_plugin_admin_page() { if (isset($_POST['submit'])) { $this->save_admin_settings(); } $options = get_option('my_custom_plugin_options', array()); ?> <div class="wrap"> <h1>My Custom Plugin Settings</h1> <form method="post" action=""> <table class="form-table"> <tr> <th><label for="api_key">API Key</label></th> <td> <input type="text" id="api_key" name="api_key" value="<?php echo esc_attr($options['api_key'] ?? ''); ?>" class="regular-text" /> </td> </tr> <tr> <th><label for="enable_feature">Enable Feature</label></th> <td> <input type="checkbox" id="enable_feature" name="enable_feature" value="1" <?php checked(1, $options['enable_feature'] ?? 0); ?> /> <label for="enable_feature">Enable this feature</label> </td> </tr> </table> <?php submit_button(); ?> </form> </div> <?php } private function save_admin_settings() { if (!current_user_can('manage_options')) { return; } $options = array( 'api_key' => sanitize_text_field($_POST['api_key'] ?? ''), 'enable_feature' => isset($_POST['enable_feature']) ? 1 : 0 ); update_option('my_custom_plugin_options', $options); echo '<div class="notice notice-success"><p>Settings saved successfully!</p></div>'; }
Step 6: Working with WordPress Hooks
Understanding WordPress hooks is crucial for plugin development:
// Action hooks - perform actions add_action('wp_head', 'my_custom_function'); add_action('init', array($this, 'my_init_function')); // Filter hooks - modify data add_filter('the_title', 'my_title_filter'); add_filter('the_content', array($this, 'my_content_filter')); // Example filter function public function my_content_filter($content) { if (is_single()) { $content .= '<div class="custom-content-footer">Custom footer content</div>'; } return $content; }
Step 7: Database Operations
Plugins often need to work with the WordPress database:
// Creating custom tables public function create_custom_table() { global $wpdb; $table_name = $wpdb->prefix . 'custom_plugin_table'; $charset_collate = $wpdb->get_charset_collate(); $sql = "CREATE TABLE $table_name ( id mediumint(9) NOT NULL AUTO_INCREMENT, title varchar(255) NOT NULL, content text NOT NULL, created_at datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id) ) $charset_collate;"; require_once(ABSPATH . 'wp-admin/includes/upgrade.php'); dbDelta($sql); } // Inserting data public function insert_custom_data($title, $content) { global $wpdb; $table_name = $wpdb->prefix . 'custom_plugin_table'; $wpdb->insert( $table_name, array( 'title' => $title, 'content' => $content ), array('%s', '%s') ); return $wpdb->insert_id; } // Retrieving data public function get_custom_data($id) { global $wpdb; $table_name = $wpdb->prefix . 'custom_plugin_table'; return $wpdb->get_row( $wpdb->prepare("SELECT * FROM $table_name WHERE id = %d", $id) ); }
Best Practices for WordPress Plugin Development
- Security First: Always sanitize and validate user input
- Use WordPress Coding Standards: Follow WordPress coding conventions
- Prefix Everything: Use unique prefixes to avoid conflicts
- Check Permissions: Always verify user capabilities
- Use Nonces: Implement nonces for form security
- Internationalization: Make your plugin translatable
- Error Handling: Implement proper error handling
- Performance: Optimize database queries and minimize HTTP requests
Testing Your Plugin
Before releasing your plugin:
- Test on different WordPress versions
- Test with popular themes and plugins
- Validate security with security plugins
- Check for PHP errors and warnings
- Test on different server configurations
Conclusion
WordPress plugin development is a powerful skill that allows you to extend WordPress functionality in countless ways. By following this guide and practicing regularly, you'll develop the skills needed to create professional, secure, and user-friendly WordPress plugins.
Remember to always prioritize security, follow WordPress best practices, and create plugins that provide real value to users.