CMS & Framework DevelopmentAdvanced
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 .= '
` with direct `$_POST` handling — you will get security wrong.
## Enqueuing CSS and JS properly
Never put `
— 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
Was this article helpful?
Your feedback helps us improve our documentation
Still need help? Submit a support ticket