--- name: wordpress-plugin-core description: | Build secure WordPress plugins with core patterns for hooks, database interactions, Settings API, custom post types, REST API, and AJAX. Covers three architecture patterns (Simple, OOP, PSR-4) and the Security Trinity. Use when creating plugins, implementing nonces/sanitization/escaping, working with $wpdb prepared statements, or troubleshooting SQL injection, XSS, CSRF vulnerabilities, or plugin activation errors. license: MIT --- # WordPress Plugin Development (Core) **Status**: Production Ready **Last Updated**: 2025-11-06 **Dependencies**: None (WordPress 5.9+, PHP 7.4+) **Latest Versions**: WordPress 6.7+, PHP 8.0+ recommended --- ## Quick Start (10 Minutes) ### 1. Choose Your Plugin Structure WordPress plugins can use three architecture patterns: - **Simple** (functions only) - For small plugins with <5 functions - **OOP** (Object-Oriented) - For medium plugins with related functionality - **PSR-4** (Namespaced + Composer autoload) - For large/modern plugins **Why this matters:** - Simple plugins are easiest to start but don't scale well - OOP provides organization without modern PHP features - PSR-4 is the modern standard (2025) and most maintainable ### 2. Create Plugin Header Every plugin MUST have a header comment in the main file: ```php " /> // 4. Sanitize Input, Escape Output $clean = sanitize_text_field( $_POST['input'] ); echo esc_html( $output ); // 5. Prepared Statements for Database global $wpdb; $results = $wpdb->get_results( $wpdb->prepare( "SELECT * FROM {$wpdb->prefix}table WHERE id = %d", $id ) ); ``` --- ## The 5-Step Security Foundation WordPress plugin security has THREE components that must ALL be present: ### Step 1: Use Unique Prefix for Everything **Why**: Prevents naming conflicts with other plugins and WordPress core. **Rules**: - 4-5 characters minimum - Apply to: functions, classes, constants, options, transients, meta keys, global variables - Avoid: `wp_`, `__`, `_`, "WordPress" ```php // GOOD function mypl_function_name() {} class MyPL_Class_Name {} define( 'MYPL_CONSTANT', 'value' ); add_option( 'mypl_option', 'value' ); set_transient( 'mypl_cache', $data, HOUR_IN_SECONDS ); // BAD function function_name() {} // No prefix, will conflict class Settings {} // Too generic ``` ### Step 2: Check Capabilities, Not Just Admin Status **ERROR**: Using `is_admin()` for permission checks ```php // WRONG - Anyone can access admin area URLs if ( is_admin() ) { // Delete user data - SECURITY HOLE } // CORRECT - Check user capability if ( current_user_can( 'manage_options' ) ) { // Delete user data - Now secure } ``` **Common Capabilities**: - `manage_options` - Administrator - `edit_posts` - Editor/Author - `publish_posts` - Author - `edit_pages` - Editor - `read` - Subscriber ### Step 3: The Security Trinity **Input → Processing → Output** each require different functions: ```php // SANITIZATION (Input) - Clean user data $name = sanitize_text_field( $_POST['name'] ); $email = sanitize_email( $_POST['email'] ); $url = esc_url_raw( $_POST['url'] ); $html = wp_kses_post( $_POST['content'] ); // Allow safe HTML $key = sanitize_key( $_POST['option'] ); $ids = array_map( 'absint', $_POST['ids'] ); // Array of integers // VALIDATION (Logic) - Verify it meets requirements if ( ! is_email( $email ) ) { wp_die( 'Invalid email' ); } // ESCAPING (Output) - Make safe for display echo esc_html( $name ); echo ''; echo '
'; echo ''; ``` **Critical Rule**: Sanitize on INPUT, escape on OUTPUT. Never trust user data. ### Step 4: Nonces (CSRF Protection) **What**: One-time tokens that prove requests came from your site. **Form Pattern**: ```php // Generate nonce in form
// Verify nonce in handler if ( ! isset( $_POST['mypl_nonce'] ) || ! wp_verify_nonce( $_POST['mypl_nonce'], 'mypl_action' ) ) { wp_die( 'Security check failed' ); } // Now safe to proceed $data = sanitize_text_field( $_POST['data'] ); ``` **AJAX Pattern**: ```javascript // JavaScript jQuery.ajax({ url: ajaxurl, data: { action: 'mypl_ajax_action', nonce: mypl_ajax_object.nonce, data: formData } }); ``` ```php // PHP Handler function mypl_ajax_handler() { check_ajax_referer( 'mypl-ajax-nonce', 'nonce' ); // Safe to proceed wp_send_json_success( array( 'message' => 'Success' ) ); } add_action( 'wp_ajax_mypl_ajax_action', 'mypl_ajax_handler' ); // Localize script with nonce wp_localize_script( 'mypl-script', 'mypl_ajax_object', array( 'ajaxurl' => admin_url( 'admin-ajax.php' ), 'nonce' => wp_create_nonce( 'mypl-ajax-nonce' ), ) ); ``` ### Step 5: Prepared Statements for Database **CRITICAL**: Always use `$wpdb->prepare()` for queries with user input. ```php global $wpdb; // WRONG - SQL Injection vulnerability $results = $wpdb->get_results( "SELECT * FROM {$wpdb->prefix}table WHERE id = {$_GET['id']}" ); // CORRECT - Prepared statement $results = $wpdb->get_results( $wpdb->prepare( "SELECT * FROM {$wpdb->prefix}table WHERE id = %d", $_GET['id'] ) ); ``` **Placeholders**: - `%s` - String - `%d` - Integer - `%f` - Float **LIKE Queries** (Special Case): ```php $search = '%' . $wpdb->esc_like( $term ) . '%'; $results = $wpdb->get_results( $wpdb->prepare( "SELECT * FROM {$wpdb->prefix}posts WHERE post_title LIKE %s", $search ) ); ``` --- ## Critical Rules ### Always Do ✅ **Use unique prefix** (4-5 chars) for all global code (functions, classes, options, transients) ✅ **Add ABSPATH check** to every PHP file: `if ( ! defined( 'ABSPATH' ) ) exit;` ✅ **Check capabilities** (`current_user_can()`) not just `is_admin()` ✅ **Verify nonces** for all forms and AJAX requests ✅ **Use $wpdb->prepare()** for all database queries with user input ✅ **Sanitize input** with `sanitize_*()` functions before saving ✅ **Escape output** with `esc_*()` functions before displaying ✅ **Flush rewrite rules** on activation when registering custom post types ✅ **Use uninstall.php** for permanent cleanup (not deactivation hook) ✅ **Follow WordPress Coding Standards** (tabs for indentation, Yoda conditions) ### Never Do ❌ **Never use extract()** - Creates security vulnerabilities ❌ **Never trust $_POST/$_GET** without sanitization ❌ **Never concatenate user input into SQL** - Always use prepare() ❌ **Never use `is_admin()` alone** for permission checks ❌ **Never output unsanitized data** - Always escape ❌ **Never use generic function/class names** - Always prefix ❌ **Never use short PHP tags** `prepare()` with placeholders ```php // VULNERABLE $wpdb->query( "DELETE FROM {$wpdb->prefix}table WHERE id = {$_GET['id']}" ); // SECURE $wpdb->query( $wpdb->prepare( "DELETE FROM {$wpdb->prefix}table WHERE id = %d", $_GET['id'] ) ); ``` ### Issue #2: XSS (Cross-Site Scripting) **Error**: Malicious JavaScript executed in user browsers **Source**: https://patchstack.com (35% of all vulnerabilities) **Why It Happens**: Outputting unsanitized user data to HTML **Prevention**: Always escape output with context-appropriate function ```php // VULNERABLE echo $_POST['name']; echo '
'; // SECURE echo esc_html( $_POST['name'] ); echo '
'; ``` ### Issue #3: CSRF (Cross-Site Request Forgery) **Error**: Unauthorized actions performed on behalf of users **Source**: https://blog.nintechnet.com/25-wordpress-plugins-vulnerable-to-csrf-attacks/ **Why It Happens**: No verification that requests originated from your site **Prevention**: Use nonces with `wp_nonce_field()` and `wp_verify_nonce()` ```php // VULNERABLE if ( $_POST['action'] == 'delete' ) { delete_user( $_POST['user_id'] ); } // SECURE if ( ! wp_verify_nonce( $_POST['nonce'], 'mypl_delete_user' ) ) { wp_die( 'Security check failed' ); } delete_user( absint( $_POST['user_id'] ) ); ``` ### Issue #4: Missing Capability Checks **Error**: Regular users can access admin functions **Source**: WordPress Security Review Guidelines **Why It Happens**: Using `is_admin()` instead of `current_user_can()` **Prevention**: Always check capabilities, not just admin context ```php // VULNERABLE if ( is_admin() ) { // Any logged-in user can trigger this } // SECURE if ( current_user_can( 'manage_options' ) ) { // Only administrators can trigger this } ``` ### Issue #5: Direct File Access **Error**: PHP files executed outside WordPress context **Source**: WordPress Plugin Handbook **Why It Happens**: No ABSPATH check at top of file **Prevention**: Add ABSPATH check to every PHP file ```php // Add to top of EVERY PHP file if ( ! defined( 'ABSPATH' ) ) { exit; } ``` ### Issue #6: Prefix Collision **Error**: Functions/classes conflict with other plugins **Source**: WordPress Coding Standards **Why It Happens**: Generic names without unique prefix **Prevention**: Use 4-5 character prefix on ALL global code ```php // CAUSES CONFLICTS function init() {} class Settings {} add_option( 'api_key', $value ); // SAFE function mypl_init() {} class MyPL_Settings {} add_option( 'mypl_api_key', $value ); ``` ### Issue #7: Rewrite Rules Not Flushed **Error**: Custom post types return 404 errors **Source**: WordPress Plugin Handbook **Why It Happens**: Forgot to flush rewrite rules after registering CPT **Prevention**: Flush on activation, clear on deactivation ```php function mypl_activate() { mypl_register_cpt(); flush_rewrite_rules(); } register_activation_hook( __FILE__, 'mypl_activate' ); function mypl_deactivate() { flush_rewrite_rules(); } register_deactivation_hook( __FILE__, 'mypl_deactivate' ); ``` ### Issue #8: Transients Not Cleaned **Error**: Database accumulates expired transients **Source**: WordPress Transients API Documentation **Why It Happens**: No cleanup on uninstall **Prevention**: Delete transients in uninstall.php ```php // uninstall.php if ( ! defined( 'WP_UNINSTALL_PLUGIN' ) ) { exit; } global $wpdb; $wpdb->query( "DELETE FROM {$wpdb->options} WHERE option_name LIKE '_transient_mypl_%'" ); $wpdb->query( "DELETE FROM {$wpdb->options} WHERE option_name LIKE '_transient_timeout_mypl_%'" ); ``` ### Issue #9: Scripts Loaded Everywhere **Error**: Performance degraded by unnecessary asset loading **Source**: WordPress Performance Best Practices **Why It Happens**: Enqueuing scripts/styles without conditional checks **Prevention**: Only load assets where needed ```php // BAD - Loads on every page add_action( 'wp_enqueue_scripts', function() { wp_enqueue_script( 'mypl-script', $url ); } ); // GOOD - Only loads on specific page add_action( 'wp_enqueue_scripts', function() { if ( is_page( 'my-page' ) ) { wp_enqueue_script( 'mypl-script', $url, array( 'jquery' ), '1.0', true ); } } ); ``` ### Issue #10: Missing Sanitization on Save **Error**: Malicious data stored in database **Source**: WordPress Data Validation **Why It Happens**: Saving $_POST data without sanitization **Prevention**: Always sanitize before saving ```php // VULNERABLE update_option( 'mypl_setting', $_POST['value'] ); // SECURE update_option( 'mypl_setting', sanitize_text_field( $_POST['value'] ) ); ``` ### Issue #11: Incorrect LIKE Queries **Error**: SQL syntax errors or injection vulnerabilities **Source**: WordPress $wpdb Documentation **Why It Happens**: LIKE wildcards not escaped properly **Prevention**: Use `$wpdb->esc_like()` ```php // WRONG $search = '%' . $term . '%'; // CORRECT $search = '%' . $wpdb->esc_like( $term ) . '%'; $results = $wpdb->get_results( $wpdb->prepare( "... WHERE title LIKE %s", $search ) ); ``` ### Issue #12: Using extract() **Error**: Variable collision and security vulnerabilities **Source**: WordPress Coding Standards **Why It Happens**: extract() creates variables from array keys **Prevention**: Never use extract(), access array elements directly ```php // DANGEROUS extract( $_POST ); // Now $any_array_key becomes a variable // SAFE $name = isset( $_POST['name'] ) ? sanitize_text_field( $_POST['name'] ) : ''; ``` ### Issue #13: Missing Permission Callback in REST API **Error**: Endpoints accessible to everyone **Source**: WordPress REST API Handbook **Why It Happens**: No `permission_callback` specified **Prevention**: Always add permission_callback ```php // VULNERABLE register_rest_route( 'myplugin/v1', '/data', array( 'callback' => 'my_callback', ) ); // SECURE register_rest_route( 'myplugin/v1', '/data', array( 'callback' => 'my_callback', 'permission_callback' => function() { return current_user_can( 'edit_posts' ); }, ) ); ``` ### Issue #14: Uninstall Hook Registered Repeatedly **Error**: Option written on every page load **Source**: WordPress Plugin Handbook **Why It Happens**: register_uninstall_hook() called in main flow **Prevention**: Use uninstall.php file instead ```php // BAD - Runs on every page load register_uninstall_hook( __FILE__, 'mypl_uninstall' ); // GOOD - Use uninstall.php file (preferred method) // Create uninstall.php in plugin root ``` ### Issue #15: Data Deleted on Deactivation **Error**: Users lose data when temporarily disabling plugin **Source**: WordPress Plugin Development Best Practices **Why It Happens**: Confusion about deactivation vs uninstall **Prevention**: Only delete data in uninstall.php, never on deactivation ```php // WRONG - Deletes user data on deactivation register_deactivation_hook( __FILE__, function() { delete_option( 'mypl_user_settings' ); } ); // CORRECT - Only clear temporary data on deactivation register_deactivation_hook( __FILE__, function() { delete_transient( 'mypl_cache' ); } ); // CORRECT - Delete all data in uninstall.php ``` ### Issue #16: Using Deprecated Functions **Error**: Plugin breaks on WordPress updates **Source**: WordPress Deprecated Functions List **Why It Happens**: Using functions removed in newer WordPress versions **Prevention**: Enable WP_DEBUG during development ```php // In wp-config.php (development only) define( 'WP_DEBUG', true ); define( 'WP_DEBUG_LOG', true ); define( 'WP_DEBUG_DISPLAY', false ); ``` ### Issue #17: Text Domain Mismatch **Error**: Translations don't load **Source**: WordPress Internationalization **Why It Happens**: Text domain doesn't match plugin slug **Prevention**: Use exact plugin slug everywhere ```php // Plugin header // Text Domain: my-plugin // In code - MUST MATCH EXACTLY __( 'Text', 'my-plugin' ); _e( 'Text', 'my-plugin' ); ``` ### Issue #18: Missing Plugin Dependencies **Error**: Fatal error when required plugin is inactive **Source**: WordPress Plugin Dependencies **Why It Happens**: No check for required plugins **Prevention**: Check for dependencies on plugins_loaded ```php add_action( 'plugins_loaded', function() { if ( ! class_exists( 'WooCommerce' ) ) { add_action( 'admin_notices', function() { echo '

My Plugin requires WooCommerce.

'; } ); return; } // Initialize plugin } ); ``` ### Issue #19: Autosave Triggering Meta Save **Error**: Meta saved multiple times, performance issues **Source**: WordPress Post Meta **Why It Happens**: No autosave check in save_post hook **Prevention**: Check for DOING_AUTOSAVE constant ```php add_action( 'save_post', function( $post_id ) { if ( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE ) { return; } // Safe to save meta } ); ``` ### Issue #20: admin-ajax.php Performance **Error**: Slow AJAX responses **Source**: https://deliciousbrains.com/comparing-wordpress-rest-api-performance-admin-ajax-php/ **Why It Happens**: admin-ajax.php loads entire WordPress core **Prevention**: Use REST API for new projects (10x faster) ```php // OLD: admin-ajax.php (still works but slower) add_action( 'wp_ajax_mypl_action', 'mypl_ajax_handler' ); // NEW: REST API (10x faster, recommended) add_action( 'rest_api_init', function() { register_rest_route( 'myplugin/v1', '/endpoint', array( 'methods' => 'POST', 'callback' => 'mypl_rest_handler', 'permission_callback' => function() { return current_user_can( 'edit_posts' ); }, ) ); } ); ``` --- ## Plugin Architecture Patterns ### Pattern 1: Simple Plugin (Functions Only) **When to use**: Small plugins with <5 functions, no complex state ```php

My Plugin Settings

define_constants(); $this->init_hooks(); } private function define_constants() { define( 'MYPL_VERSION', '1.0.0' ); define( 'MYPL_PLUGIN_DIR', plugin_dir_path( __FILE__ ) ); define( 'MYPL_PLUGIN_URL', plugin_dir_url( __FILE__ ) ); } private function init_hooks() { add_action( 'init', array( $this, 'init' ) ); add_action( 'admin_menu', array( $this, 'admin_menu' ) ); } public function init() { // Initialization code } public function admin_menu() { add_options_page( 'My Plugin', 'My Plugin', 'manage_options', 'my-plugin', array( $this, 'settings_page' ) ); } public function settings_page() { ?>

My Plugin Settings

=7.4" } } ``` **my-plugin.php**: ```php init(); } private function init() { new Admin(); new Frontend(); } } MyPlugin::get_instance(); ``` **src/Admin.php**: ```php

My Plugin Settings

array( 'name' => 'Books', 'singular_name' => 'Book', 'add_new_item' => 'Add New Book', ), 'public' => true, 'has_archive' => true, 'show_in_rest' => true, // Gutenberg support 'supports' => array( 'title', 'editor', 'thumbnail', 'excerpt' ), 'rewrite' => array( 'slug' => 'books' ), 'menu_icon' => 'dashicons-book', ) ); } add_action( 'init', 'mypl_register_cpt' ); // CRITICAL: Flush rewrite rules on activation function mypl_activate() { mypl_register_cpt(); flush_rewrite_rules(); } register_activation_hook( __FILE__, 'mypl_activate' ); function mypl_deactivate() { flush_rewrite_rules(); } register_deactivation_hook( __FILE__, 'mypl_deactivate' ); ``` ### Pattern 2: Custom Taxonomies ```php function mypl_register_taxonomy() { register_taxonomy( 'genre', 'book', array( 'labels' => array( 'name' => 'Genres', 'singular_name' => 'Genre', ), 'hierarchical' => true, // Like categories 'show_in_rest' => true, 'rewrite' => array( 'slug' => 'genre' ), ) ); } add_action( 'init', 'mypl_register_taxonomy' ); ``` ### Pattern 3: Meta Boxes ```php function mypl_add_meta_box() { add_meta_box( 'book_details', 'Book Details', 'mypl_meta_box_html', 'book', 'normal', 'high' ); } add_action( 'add_meta_boxes', 'mypl_add_meta_box' ); function mypl_meta_box_html( $post ) { $isbn = get_post_meta( $post->ID, '_book_isbn', true ); wp_nonce_field( 'mypl_save_meta', 'mypl_meta_nonce' ); ?> 'string', 'sanitize_callback' => 'sanitize_text_field', 'default' => '', ) ); add_settings_section( 'mypl_section', 'API Settings', 'mypl_section_callback', 'my-plugin' ); add_settings_field( 'mypl_api_key', 'API Key', 'mypl_field_callback', 'my-plugin', 'mypl_section' ); } add_action( 'admin_init', 'mypl_register_settings' ); function mypl_section_callback() { echo '

Configure your API settings.

'; } function mypl_field_callback() { $value = get_option( 'mypl_api_key' ); ?>

WP_REST_Server::READABLE, 'callback' => 'mypl_rest_callback', 'permission_callback' => function() { return current_user_can( 'edit_posts' ); }, 'args' => array( 'id' => array( 'required' => true, 'validate_callback' => function( $param ) { return is_numeric( $param ); }, 'sanitize_callback' => 'absint', ), ), ) ); } ); function mypl_rest_callback( $request ) { $id = $request->get_param( 'id' ); // Process... return new WP_REST_Response( array( 'success' => true, 'data' => $data, ), 200 ); } ``` ### Pattern 6: AJAX Handlers (Legacy) ```php // Enqueue script with localized data function mypl_enqueue_ajax_script() { wp_enqueue_script( 'mypl-ajax', plugins_url( 'js/ajax.js', __FILE__ ), array( 'jquery' ), '1.0', true ); wp_localize_script( 'mypl-ajax', 'mypl_ajax_object', array( 'ajaxurl' => admin_url( 'admin-ajax.php' ), 'nonce' => wp_create_nonce( 'mypl-ajax-nonce' ), ) ); } add_action( 'wp_enqueue_scripts', 'mypl_enqueue_ajax_script' ); // AJAX handler (logged-in users) function mypl_ajax_handler() { check_ajax_referer( 'mypl-ajax-nonce', 'nonce' ); $data = sanitize_text_field( $_POST['data'] ); // Process... wp_send_json_success( array( 'message' => 'Success' ) ); } add_action( 'wp_ajax_mypl_action', 'mypl_ajax_handler' ); // AJAX handler (logged-out users) add_action( 'wp_ajax_nopriv_mypl_action', 'mypl_ajax_handler' ); ``` **JavaScript (js/ajax.js)**: ```javascript jQuery(document).ready(function($) { $('#my-button').on('click', function() { $.ajax({ url: mypl_ajax_object.ajaxurl, type: 'POST', data: { action: 'mypl_action', nonce: mypl_ajax_object.nonce, data: 'value' }, success: function(response) { console.log(response.data.message); } }); }); }); ``` ### Pattern 7: Custom Database Tables ```php function mypl_create_tables() { global $wpdb; $table_name = $wpdb->prefix . 'mypl_data'; $charset_collate = $wpdb->get_charset_collate(); $sql = "CREATE TABLE $table_name ( id bigint(20) NOT NULL AUTO_INCREMENT, user_id bigint(20) NOT NULL, data text NOT NULL, created datetime DEFAULT CURRENT_TIMESTAMP NOT NULL, PRIMARY KEY (id), KEY user_id (user_id) ) $charset_collate;"; require_once ABSPATH . 'wp-admin/includes/upgrade.php'; dbDelta( $sql ); add_option( 'mypl_db_version', '1.0' ); } // Create tables on activation register_activation_hook( __FILE__, 'mypl_create_tables' ); ``` ### Pattern 8: Transients for Caching ```php function mypl_get_expensive_data() { // Try to get cached data $data = get_transient( 'mypl_expensive_data' ); if ( false === $data ) { // Not cached - regenerate $data = perform_expensive_operation(); // Cache for 12 hours set_transient( 'mypl_expensive_data', $data, 12 * HOUR_IN_SECONDS ); } return $data; } // Clear cache when data changes function mypl_clear_cache() { delete_transient( 'mypl_expensive_data' ); } add_action( 'save_post', 'mypl_clear_cache' ); ``` --- ## Using Bundled Resources ### Templates (templates/) Use these production-ready templates to scaffold plugins quickly: - `templates/plugin-simple/` - Simple plugin with functions - `templates/plugin-oop/` - Object-oriented plugin structure - `templates/plugin-psr4/` - Modern PSR-4 plugin with Composer - `templates/examples/meta-box.php` - Meta box implementation - `templates/examples/settings-page.php` - Settings API page - `templates/examples/custom-post-type.php` - CPT registration - `templates/examples/rest-endpoint.php` - REST API endpoint - `templates/examples/ajax-handler.php` - AJAX implementation **When Claude should use these**: When creating new plugins or implementing specific functionality patterns. ### Scripts (scripts/) - `scripts/scaffold-plugin.sh` - Interactive plugin scaffolding - `scripts/check-security.sh` - Security audit for common issues - `scripts/validate-headers.sh` - Verify plugin headers **Example Usage:** ```bash # Scaffold new plugin ./scripts/scaffold-plugin.sh my-plugin simple # Check for security issues ./scripts/check-security.sh my-plugin.php # Validate plugin headers ./scripts/validate-headers.sh my-plugin.php ``` ### References (references/) Detailed documentation that Claude can load when needed: - `references/security-checklist.md` - Complete security audit checklist - `references/hooks-reference.md` - Common WordPress hooks and filters - `references/sanitization-guide.md` - All sanitization/escaping functions - `references/wpdb-patterns.md` - Database query patterns - `references/common-errors.md` - Extended error prevention guide **When Claude should load these**: When dealing with security issues, choosing the right hook, sanitizing specific data types, writing database queries, or debugging common errors. --- ## Advanced Topics ### Internationalization (i18n) ```php // Load text domain function mypl_load_textdomain() { load_plugin_textdomain( 'my-plugin', false, dirname( plugin_basename( __FILE__ ) ) . '/languages' ); } add_action( 'plugins_loaded', 'mypl_load_textdomain' ); // Translatable strings __( 'Text', 'my-plugin' ); // Returns translated string _e( 'Text', 'my-plugin' ); // Echoes translated string _n( 'One item', '%d items', $count, 'my-plugin' ); // Plural forms esc_html__( 'Text', 'my-plugin' ); // Translate and escape esc_html_e( 'Text', 'my-plugin' ); // Translate, escape, and echo ``` ### WP-CLI Commands ```php if ( defined( 'WP_CLI' ) && WP_CLI ) { class MyPL_CLI_Command { /** * Process data * * ## EXAMPLES * * wp mypl process --limit=100 * * @param array $args * @param array $assoc_args */ public function process( $args, $assoc_args ) { $limit = isset( $assoc_args['limit'] ) ? absint( $assoc_args['limit'] ) : 10; WP_CLI::line( "Processing $limit items..." ); // Process... WP_CLI::success( 'Processing complete!' ); } } WP_CLI::add_command( 'mypl', 'MyPL_CLI_Command' ); } ``` ### Scheduled Events (Cron) ```php // Schedule event on activation function mypl_activate() { if ( ! wp_next_scheduled( 'mypl_daily_task' ) ) { wp_schedule_event( time(), 'daily', 'mypl_daily_task' ); } } register_activation_hook( __FILE__, 'mypl_activate' ); // Clear event on deactivation function mypl_deactivate() { wp_clear_scheduled_hook( 'mypl_daily_task' ); } register_deactivation_hook( __FILE__, 'mypl_deactivate' ); // Hook to scheduled event function mypl_do_daily_task() { // Perform task } add_action( 'mypl_daily_task', 'mypl_do_daily_task' ); ``` ### Plugin Dependencies Check ```php add_action( 'admin_init', function() { // Check for WooCommerce if ( ! class_exists( 'WooCommerce' ) ) { deactivate_plugins( plugin_basename( __FILE__ ) ); add_action( 'admin_notices', function() { echo '

My Plugin requires WooCommerce to be installed and active.

'; } ); if ( isset( $_GET['activate'] ) ) { unset( $_GET['activate'] ); } } } ); ``` --- ## Distribution & Auto-Updates ### Enabling GitHub Auto-Updates Plugins hosted outside WordPress.org can still provide automatic updates using **Plugin Update Checker** by YahnisElsts. This is the recommended solution for most use cases. **Quick Start:** ```php // 1. Install library (git submodule or Composer) git submodule add https://github.com/YahnisElsts/plugin-update-checker.git // 2. Add to main plugin file require plugin_dir_path( __FILE__ ) . 'plugin-update-checker/plugin-update-checker.php'; use YahnisElsts\PluginUpdateChecker\v5\PucFactory; $updateChecker = PucFactory::buildUpdateChecker( 'https://github.com/yourusername/your-plugin/', __FILE__, 'your-plugin-slug' ); // Use GitHub Releases (recommended) $updateChecker->getVcsApi()->enableReleaseAssets(); // For private repos, use token from wp-config.php if ( defined( 'YOUR_PLUGIN_GITHUB_TOKEN' ) ) { $updateChecker->setAuthentication( YOUR_PLUGIN_GITHUB_TOKEN ); } ``` **Deployment:** ```bash # 1. Update version in plugin header # 2. Commit and tag git add my-plugin.php git commit -m "Bump version to 1.0.1" git tag 1.0.1 git push origin main git push origin 1.0.1 # 3. Create GitHub Release (optional but recommended) # - Upload pre-built ZIP file (exclude .git, tests, etc.) # - Add release notes for users ``` **Key Features:** ✅ Works with GitHub, GitLab, BitBucket, or custom servers ✅ Supports public and private repositories ✅ Uses GitHub Releases or tags for versioning ✅ Secure HTTPS-based updates ✅ Optional license key integration ✅ Professional release notes and changelogs ✅ ~100KB library footprint **Alternative Solutions:** 1. **Git Updater** (user-installable plugin, no coding required) 2. **Custom Update Server** (full control, requires hosting) 3. **Freemius** (commercial, includes licensing and payments) **Comprehensive Resources:** - **Complete Guide**: See `references/github-auto-updates.md` (21 pages, all approaches) - **Implementation Examples**: See `examples/github-updater.php` (10 examples) - **Security Best Practices**: Checksums, signing, token storage, rate limiting - **Template Integration**: All 3 plugin templates include setup instructions **Security Considerations:** - ✅ Always use HTTPS for repository URLs - ✅ Never hardcode authentication tokens (use wp-config.php) - ✅ Implement license validation before offering updates - ✅ Optional: Add checksums for file verification - ✅ Rate limit update checks to avoid API throttling - ✅ Clear cached update data after installation **When to Use Each Approach:** | Use Case | Recommended Solution | |----------|---------------------| | Open source, public repo | Plugin Update Checker | | Private plugin, client work | Plugin Update Checker + private repo | | Commercial plugin | Freemius or Custom Server | | Multi-platform Git hosting | Git Updater | | Custom licensing needs | Custom Update Server | **ZIP Structure Requirement:** ``` plugin.zip └── my-plugin/ ← Plugin folder MUST be inside ZIP ├── my-plugin.php ├── readme.txt └── ... ``` Incorrect structure will cause WordPress to create a random folder name and break the plugin! --- ## Dependencies **Required**: - WordPress 5.9+ (recommend 6.7+) - PHP 7.4+ (recommend 8.0+) **Optional**: - Composer 2.0+ - For PSR-4 autoloading - WP-CLI 2.0+ - For command-line plugin management - Query Monitor - For debugging and performance analysis --- ## Official Documentation - **WordPress Plugin Handbook**: https://developer.wordpress.org/plugins/ - **WordPress Coding Standards**: https://developer.wordpress.org/coding-standards/ - **WordPress REST API**: https://developer.wordpress.org/rest-api/ - **WordPress Database Class ($wpdb)**: https://developer.wordpress.org/reference/classes/wpdb/ - **WordPress Security**: https://developer.wordpress.org/apis/security/ - **Settings API**: https://developer.wordpress.org/plugins/settings/settings-api/ - **Custom Post Types**: https://developer.wordpress.org/plugins/post-types/ - **Transients API**: https://developer.wordpress.org/apis/transients/ - **Context7 Library ID**: /websites/developer_wordpress --- ## Troubleshooting ### Problem: Plugin causes fatal error **Solution**: 1. Enable WP_DEBUG in wp-config.php 2. Check error log at wp-content/debug.log 3. Verify all class/function names are prefixed 4. Check for missing dependencies ### Problem: 404 errors on custom post type pages **Solution**: Flush rewrite rules ```php // Temporarily add to wp-admin flush_rewrite_rules(); // Remove after visiting wp-admin once ``` ### Problem: Nonce verification always fails **Solution**: 1. Check nonce name matches in field and verification 2. Verify using correct action name 3. Ensure nonce hasn't expired (24 hour default) ### Problem: AJAX returns 0 or -1 **Solution**: 1. Verify action name matches hook: `wp_ajax_{action}` 2. Check nonce is being sent and verified 3. Ensure handler function exists and is hooked correctly ### Problem: Sanitization stripping HTML **Solution**: Use `wp_kses_post()` instead of `sanitize_text_field()` to allow safe HTML ### Problem: Database queries not working **Solution**: 1. Always use `$wpdb->prepare()` for queries with variables 2. Check table name includes `$wpdb->prefix` 3. Verify column names and syntax --- ## Complete Setup Checklist Use this checklist to verify your plugin: - [ ] Plugin header complete with all fields - [ ] ABSPATH check at top of every PHP file - [ ] All functions/classes use unique prefix - [ ] All forms have nonce verification - [ ] All user input is sanitized - [ ] All output is escaped - [ ] All database queries use $wpdb->prepare() - [ ] Capability checks (not just is_admin()) - [ ] Custom post types flush rewrite rules on activation - [ ] Deactivation hook only clears temporary data - [ ] uninstall.php handles permanent cleanup - [ ] Text domain matches plugin slug - [ ] Scripts/styles only load where needed - [ ] WP_DEBUG enabled during development - [ ] Tested with Query Monitor for performance - [ ] No deprecated function warnings - [ ] Works with latest WordPress version --- **Questions? Issues?** 1. Check `references/common-errors.md` for extended troubleshooting 2. Verify all steps in the security foundation 3. Check official docs: https://developer.wordpress.org/plugins/ 4. Enable WP_DEBUG and check debug.log 5. Use Query Monitor plugin to debug hooks and queries