WordPress Core Development Skill
Core WordPress plugin development knowledge. Focus on fundamentals, security, and standard patterns.
Target: WordPress 6.8+
WordPress Plugin File Structure
Standard organization for WordPress plugins:
plugin-name/ ├── plugin-name.php # Main plugin file (header, activation) ├── uninstall.php # Uninstall cleanup (optional) ├── includes/ │ ├── class-plugin-name.php # Main plugin class │ ├── class-activator.php # Activation logic │ └── class-deactivator.php # Deactivation logic ├── admin/ │ ├── class-admin.php # Admin-specific functionality │ ├── css/ │ ├── js/ │ └── partials/ # Admin view templates ├── public/ │ ├── class-public.php # Public-facing functionality │ ├── css/ │ ├── js/ │ └── partials/ # Public view templates └── languages/ # Translation files
Key Principles:
-
Main plugin file contains header and initialization only
-
Separate admin and public functionality
-
Use classes for organization (recommended)
-
Follow WordPress naming conventions (lowercase, hyphens)
Plugin Header Format
Every plugin must have a header in the main file:
<?php /**
- Plugin Name: Your Plugin Name
- Plugin URI: https://example.com/plugin-name
- Description: Brief description of what the plugin does
- Version: 1.0.0
- Author: Your Name
- Author URI: https://example.com
- License: GPL-2.0+
- License URI: http://www.gnu.org/licenses/gpl-2.0.txt
- Text Domain: plugin-name
- Domain Path: /languages */
// If this file is called directly, abort. if ( ! defined( 'WPINC' ) ) { die; }
Required Fields: Plugin Name Recommended: Description, Version, Author, License, Text Domain
Security Principles (CRITICAL)
WordPress security follows three golden rules:
- Sanitize Input (Trust Nothing)
Always sanitize data coming from users:
// Text input $clean_text = sanitize_text_field( $_POST['user_input'] );
// Email $clean_email = sanitize_email( $_POST['email'] );
// URL $clean_url = esc_url_raw( $_POST['url'] );
// Integer $clean_id = absint( $_POST['post_id'] );
// Array of text fields $clean_array = array_map( 'sanitize_text_field', $_POST['items'] );
Common Functions:
-
sanitize_text_field()
-
Strips tags, scripts
-
sanitize_email()
-
Validates email format
-
sanitize_key()
-
Lowercase alphanumeric, dashes, underscores
-
sanitize_title()
-
Creates slug-safe string
-
esc_url_raw()
-
Database-safe URL
-
absint()
-
Absolute integer (positive only)
-
intval()
-
Integer conversion
- Escape Output (Never Trust Data)
Always escape data when outputting to HTML:
// HTML content echo esc_html( $user_data );
// HTML attributes echo '<input value="' . esc_attr( $value ) . '">';
// URLs echo '<a href="' . esc_url( $link ) . '">Link</a>';
// JavaScript echo '<script>var data = ' . wp_json_encode( $data ) . ';</script>';
// Textarea echo '<textarea>' . esc_textarea( $content ) . '</textarea>';
Common Functions:
-
esc_html()
-
Escapes HTML (converts < > & " ')
-
esc_attr()
-
Escapes HTML attributes
-
esc_url()
-
Escapes URLs for output
-
esc_js()
-
Escapes JavaScript strings
-
esc_textarea()
-
Escapes textarea content
-
wp_kses_post()
-
Allows safe HTML (like posts)
- Nonces (Verify Intent)
Use nonces to prevent CSRF attacks:
// Creating a nonce (in form) wp_nonce_field( 'my_action_name', 'my_nonce_field' );
// Verifying a nonce (on submission) if ( ! isset( $_POST['my_nonce_field'] ) || ! wp_verify_nonce( $_POST['my_nonce_field'], 'my_action_name' ) ) { die( 'Security check failed' ); }
// URL nonces $url = wp_nonce_url( 'admin.php?action=delete&id=123', 'delete_item_123' );
// Verify URL nonce if ( ! isset( $_GET['_wpnonce'] ) || ! wp_verify_nonce( $_GET['_wpnonce'], 'delete_item_123' ) ) { die( 'Security check failed' ); }
- Capability Checks (Authorization)
Verify user has permission:
// Check if user can perform action if ( ! current_user_can( 'manage_options' ) ) { wp_die( 'You do not have sufficient permissions' ); }
// Common capabilities current_user_can( 'edit_posts' ); current_user_can( 'publish_pages' ); current_user_can( 'edit_others_posts' );
Security Checklist:
-
All user input sanitized
-
All output escaped
-
Nonces verify form submissions
-
Capability checks on all admin actions
-
Direct file access prevented (defined( 'WPINC' ) )
Hooks System (Actions and Filters)
WordPress uses hooks to allow plugins to modify behavior.
Actions (Do Something)
Execute code at specific points:
// Add an action add_action( 'init', 'my_function' ); add_action( 'wp_enqueue_scripts', 'my_enqueue_function' ); add_action( 'admin_menu', 'my_menu_function' );
// Custom action (for your plugin) do_action( 'my_plugin_custom_action', $param1, $param2 );
Common Action Hooks:
-
init
-
WordPress initialized, user authenticated
-
admin_init
-
Admin area initialized
-
admin_menu
-
Time to add admin menu items
-
wp_enqueue_scripts
-
Enqueue public scripts/styles
-
admin_enqueue_scripts
-
Enqueue admin scripts/styles
-
wp_head
-
Output to <head> section
-
wp_footer
-
Output before </body>
-
save_post
-
Post is being saved
-
admin_notices
-
Display admin notices
Filters (Modify Data)
Modify values before they're used:
// Add a filter add_filter( 'the_content', 'my_content_filter' ); add_filter( 'the_title', 'my_title_filter', 10, 2 );
function my_content_filter( $content ) { // Modify and return content return $content . '<p>Added text</p>'; }
// Custom filter (for your plugin) $value = apply_filters( 'my_plugin_custom_filter', $value, $arg1, $arg2 );
Common Filter Hooks:
-
the_content
-
Post content
-
the_title
-
Post title
-
the_excerpt
-
Post excerpt
-
wp_mail
-
Email parameters
-
upload_mimes
-
Allowed upload types
-
login_redirect
-
Where to redirect after login
Priority and Arguments:
// Priority: 10 is default, lower runs earlier add_filter( 'the_title', 'my_filter', 5 ); // Runs early
// Accept multiple arguments add_filter( 'the_title', 'my_filter', 10, 2 ); function my_filter( $title, $post_id ) { return $title; }
Database Interactions (wpdb)
Use $wpdb global for database queries.
Basic Queries
global $wpdb;
// Get single value $count = $wpdb->get_var( "SELECT COUNT(*) FROM {$wpdb->posts}" );
// Get single row $post = $wpdb->get_row( "SELECT * FROM {$wpdb->posts} WHERE ID = 1" );
// Get multiple rows $posts = $wpdb->get_results( "SELECT * FROM {$wpdb->posts} WHERE post_status = 'publish'" );
// Get column $titles = $wpdb->get_col( "SELECT post_title FROM {$wpdb->posts}" );
Prepared Statements (ALWAYS)
Never use string concatenation with user input:
// WRONG - SQL Injection vulnerability $wpdb->get_results( "SELECT * FROM {$wpdb->posts} WHERE ID = {$_GET['id']}" );
// CORRECT - Prepared statement $id = absint( $_GET['id'] ); $wpdb->get_results( $wpdb->prepare( "SELECT * FROM {$wpdb->posts} WHERE ID = %d", $id ) );
// Multiple placeholders $wpdb->get_results( $wpdb->prepare( "SELECT * FROM {$wpdb->posts} WHERE post_type = %s AND post_status = %s", $post_type, $status ) );
Placeholders:
-
%s
-
String
-
%d
-
Integer
-
%f
-
Float
Insert/Update/Delete
// Insert $wpdb->insert( $wpdb->prefix . 'my_table', array( 'column1' => 'value1', 'column2' => 123 ), array( '%s', '%d' ) // Format );
// Update $wpdb->update( $wpdb->prefix . 'my_table', array( 'column1' => 'new_value' ), // Data array( 'ID' => 123 ), // Where array( '%s' ), // Data format array( '%d' ) // Where format );
// Delete $wpdb->delete( $wpdb->prefix . 'my_table', array( 'ID' => 123 ), array( '%d' ) );
Table Prefix: Always use $wpdb->prefix for custom tables
WordPress Coding Standards
Follow WordPress PHP coding standards:
Naming Conventions:
-
Functions: lowercase_with_underscores()
-
Classes: Class_Name_With_Underscores
-
Constants: UPPERCASE_WITH_UNDERSCORES
-
Files: lowercase-with-hyphens.php
Indentation: Tabs (not spaces)
Bracing:
if ( condition ) { // Code }
Spacing:
// Always space after control structures if ( condition ) { // Code }
// Space around operators $x = $y + $z;
// No space inside parentheses for function calls my_function( $arg1, $arg2 );
Yoda Conditions (constants on left):
if ( 'yes' === $value ) { // Prevents accidental assignment }
Helper Functions
Plugin paths:
plugin_dir_path( FILE ); // /path/to/wp-content/plugins/my-plugin/ plugin_dir_url( FILE ); // https://example.com/wp-content/plugins/my-plugin/ plugin_basename( FILE ); // my-plugin/my-plugin.php
Get options:
get_option( 'option_name', 'default_value' ); update_option( 'option_name', $new_value ); delete_option( 'option_name' );
Post meta:
get_post_meta( $post_id, 'meta_key', true ); // Single value update_post_meta( $post_id, 'meta_key', $value ); delete_post_meta( $post_id, 'meta_key' );
Transients (temporary cached data):
set_transient( 'my_transient', $value, 3600 ); // 1 hour $value = get_transient( 'my_transient' ); delete_transient( 'my_transient' );
Progressive Disclosure
For advanced topics, see:
-
See {baseDir}/references/security-patterns.md for advanced security patterns
-
See {baseDir}/references/hooks-api.md for comprehensive hooks reference
-
See {baseDir}/references/database-patterns.md for complex database operations
Related Skills
-
wordpress-blocks - Block development, Block Hooks API, Interactivity API
-
wordpress-modern - Performance optimization, modern WP 6.8 features
Best Practices Summary
-
Security First: Sanitize input, escape output, verify nonces, check capabilities
-
Use WordPress APIs: Don't reinvent the wheel, use built-in functions
-
Follow Standards: WordPress coding standards for consistency
-
Prepare Statements: Always use $wpdb->prepare() for queries
-
Prefix Everything: Avoid naming conflicts with unique prefixes
-
Test: Test activation, deactivation, different user roles
-
Document: Comment complex logic, use PHPDoc blocks