Client Area

WordPress Custom Plugin Development Basics (Hooks, Actions, and Security)

ByDomain India Team
9 min read22 Apr 20262 views
# WordPress Custom Plugin Development Basics (Hooks, Actions, and Security) Writing a WordPress plugin is how you extend WordPress without touching core files or theme code. This guide walks through the minimum you need to build a useful plugin — file structure, activation hooks, admin settings pages, actions vs filters, and the security patterns that separate safe plugins from the ones that introduce vulnerabilities. ## When to write a plugin vs. edit the theme A common mistake: putting custom functionality in `functions.php` of your active theme. Problems with that approach: - When you switch themes, the functionality disappears - Theme updates can overwrite your changes - Multi-site networks cannot easily share theme-local code Plugins solve all three. Write a plugin when the functionality: - Is not strictly visual (belongs to site behaviour, not theme presentation) - Should survive a theme change - Might be shared across multiple sites Rule of thumb: if it defines a custom post type, adds an admin menu, integrates an external API, or changes how WordPress runs, it belongs in a plugin. ## Plugin file structure The minimum viable plugin is one PHP file in `/wp-content/plugins/your-plugin-slug/your-plugin-slug.php`: ```php get_charset_collate(); $sql = "CREATE TABLE IF NOT EXISTS {$wpdb->prefix}mcp_logs ( id BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT, user_id BIGINT(20) UNSIGNED NOT NULL, action VARCHAR(100) NOT NULL, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY user_id (user_id) ) $charset_collate;"; require_once ABSPATH . 'wp-admin/includes/upgrade.php'; dbDelta($sql); } // Runs when someone deactivates the plugin register_deactivation_hook(__FILE__, 'mcp_deactivate'); function mcp_deactivate() { // Temporary cleanup — users expect data to survive deactivation flush_rewrite_rules(); } ``` For truly removing data, use `uninstall.php` (separate file, runs only when plugin is deleted from the Plugins screen): ```php query("DROP TABLE IF EXISTS {$wpdb->prefix}mcp_logs"); delete_option('mcp_settings'); ``` ## Actions vs filters — the core extension mechanism WordPress exposes hundreds of hooks. Two kinds: **Actions** — "something happened, do your thing." Return nothing; fire side effects. ```php add_action('init', 'mcp_register_post_type'); function mcp_register_post_type() { register_post_type('mcp_project', [ 'labels' => ['name' => 'Projects'], 'public' => true, 'show_in_rest' => true, ]); } ``` Common actions: `init`, `wp_enqueue_scripts`, `admin_menu`, `save_post`, `wp_login`. **Filters** — "something is about to happen, modify this value first." Receive a value, return a modified value. ```php add_filter('the_content', 'mcp_append_signature'); function mcp_append_signature($content) { if (is_single()) { $content .= '

— Posted from My Plugin

'; } return $content; } ``` Common filters: `the_content`, `the_title`, `wp_nav_menu_items`, `pre_get_posts`. ### Priority and arg count Both `add_action` and `add_filter` accept optional 3rd and 4th parameters — priority (default 10) and number of arguments (default 1): ```php add_action('save_post', 'mcp_on_save', 20, 2); function mcp_on_save($post_id, $post) { // runs after default priority-10 handlers } ``` Higher priority number = runs later. ## Admin menu + settings page Registering an admin menu item: ```php add_action('admin_menu', 'mcp_admin_menu'); function mcp_admin_menu() { add_menu_page( 'My Custom Plugin Settings', // page title 'My Plugin', // menu title 'manage_options', // required capability 'mcp-settings', // menu slug 'mcp_settings_page', // callback 'dashicons-admin-plugins', // icon 65 // position ); } ``` The settings page itself, using the Settings API: ```php add_action('admin_init', 'mcp_register_settings'); function mcp_register_settings() { register_setting('mcp_options', 'mcp_options', [ 'sanitize_callback' => 'mcp_sanitize_options', ]); add_settings_section('mcp_main', 'Main Settings', null, 'mcp-settings'); add_settings_field('mcp_api_key', 'API Key', 'mcp_api_key_callback', 'mcp-settings', 'mcp_main'); } function mcp_api_key_callback() { $options = get_option('mcp_options', []); $value = esc_attr($options['api_key'] ?? ''); echo ""; } function mcp_sanitize_options($input) { $clean = []; $clean['api_key'] = sanitize_text_field($input['api_key'] ?? ''); return $clean; } function mcp_settings_page() { if (!current_user_can('manage_options')) return; ?>

My Plugin Settings

` with direct `$_POST` handling — you will get security wrong. ## Enqueuing CSS and JS properly Never put `

Was this article helpful?

Your feedback helps us improve our documentation

Still need help? Submit a support ticket