wordpress-plugin-core
π―Skillfrom ovachiever/droid-tings
Builds secure WordPress plugins with core patterns, covering hooks, database interactions, Settings API, custom post types, REST API, and AJAX across three architecture patterns.
Part of
ovachiever/droid-tings(370 items)
Installation
git clone https://github.com/ovachiever/droid-tings.gitSkill Details
|
Overview
# 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
/**
* Plugin Name: My Awesome Plugin
* Plugin URI: https://example.com/my-plugin/
* Description: Brief description of what this plugin does.
* Version: 1.0.0
* Requires at least: 5.9
* Requires PHP: 7.4
* Author: Your Name
* Author URI: https://yoursite.com/
* License: GPL v2 or later
* License URI: https://www.gnu.org/licenses/gpl-2.0.html
* Text Domain: my-plugin
* Domain Path: /languages
*/
// Exit if accessed directly
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
```
CRITICAL:
- Plugin Name is the ONLY required field
- Text Domain must match plugin slug exactly (for translations)
- Always add ABSPATH check to prevent direct file access
3. Implement The Security Foundation
Before writing ANY functionality, implement these 5 security essentials:
```php
// 1. Unique Prefix (4-5 chars minimum)
define( 'MYPL_VERSION', '1.0.0' );
function mypl_init() {
// Your code
}
add_action( 'init', 'mypl_init' );
// 2. ABSPATH Check (every PHP file)
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
// 3. Nonces for Forms
// 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- Administratoredit_posts- Editor/Authorpublish_posts- Authoredit_pages- Editorread- 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. 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' ), ) ); ``` CRITICAL: Always use ```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: 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 ) ); ``` ---Step 4: Nonces (CSRF Protection)
Step 5: Prepared Statements for Database
$wpdb->prepare() for queries with user input.%s - String%d - Integer%f - Float
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 or = - Use only
β Never delete user data on deactivation - Only on uninstall
β Never register uninstall hook repeatedly - Only once on activation
β Never use register_uninstall_hook() in main flow - Use uninstall.php instead
---
Known Issues Prevention
This skill prevents 20 documented issues:
Issue #1: SQL Injection
Error: Database compromised via unescaped user input
Source: https://patchstack.com/articles/sql-injection/ (15% of all vulnerabilities)
Why It Happens: Direct concatenation of user input into SQL queries
Prevention: Always use $wpdb->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 ' ``` 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 ```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'] ) ); ``` Error: Regular users can access admin functions Source: WordPress Security Review Guidelines Why It Happens: Using 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 } ``` 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; } ``` 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 ); ``` 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' ); ``` 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_%'" ); ``` 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 ); } } ); ``` 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'] ) ); ``` Error: SQL syntax errors or injection vulnerabilities Source: WordPress $wpdb Documentation Why It Happens: LIKE wildcards not escaped properly Prevention: Use ```php // WRONG $search = '%' . $term . '%'; // CORRECT $search = '%' . $wpdb->esc_like( $term ) . '%'; $results = $wpdb->get_results( $wpdb->prepare( "... WHERE title LIKE %s", $search ) ); ``` 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'] ) : ''; ``` Error: Endpoints accessible to everyone Source: WordPress REST API Handbook Why It Happens: No 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' ); }, ) ); ``` 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 ``` 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 ``` 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 ); ``` 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' ); ``` 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 } ); ``` 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 } ); ``` 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' ); }, ) ); } ); ``` --- When to use: Small plugins with <5 functions, no complex state ```php /** * Plugin Name: Simple Plugin */ if ( ! defined( 'ABSPATH' ) ) { exit; } function mypl_init() { // Your code here } add_action( 'init', 'mypl_init' ); function mypl_admin_menu() { add_options_page( 'My Plugin', 'My Plugin', 'manage_options', 'my-plugin', 'mypl_settings_page' ); } add_action( 'admin_menu', 'mypl_admin_menu' ); function mypl_settings_page() { ?> } ``` When to use: Medium plugins with related functionality, need organization ```php /** * Plugin Name: OOP Plugin */ if ( ! defined( 'ABSPATH' ) ) { exit; } class MyPL_Plugin { private static $instance = null; public static function get_instance() { if ( null === self::$instance ) { self::$instance = new self(); } return self::$instance; } private function __construct() { $this->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() { ?> } } // Initialize plugin function mypl() { return MyPL_Plugin::get_instance(); } mypl(); ``` When to use: Large/modern plugins, team development, 2025+ best practice Directory Structure: ``` my-plugin/ βββ my-plugin.php # Main file βββ composer.json # Autoloading config βββ src/ # PSR-4 autoloaded classes β βββ Admin.php β βββ Frontend.php β βββ Settings.php βββ languages/ βββ uninstall.php ``` composer.json: ```json { "name": "my-vendor/my-plugin", "autoload": { "psr-4": { "MyPlugin\\": "src/" } }, "require": { "php": ">=7.4" } } ``` my-plugin.php: ```php /** * Plugin Name: PSR-4 Plugin */ if ( ! defined( 'ABSPATH' ) ) { exit; } // Composer autoloader require_once __DIR__ . '/vendor/autoload.php'; use MyPlugin\Admin; use MyPlugin\Frontend; class MyPlugin { private static $instance = null; public static function get_instance() { if ( null === self::$instance ) { self::$instance = new self(); } return self::$instance; } private function __construct() { $this->init(); } private function init() { new Admin(); new Frontend(); } } MyPlugin::get_instance(); ``` src/Admin.php: ```php namespace MyPlugin; class Admin { public function __construct() { add_action( 'admin_menu', array( $this, 'add_menu' ) ); } public function add_menu() { add_options_page( 'My Plugin', 'My Plugin', 'manage_options', 'my-plugin', array( $this, 'settings_page' ) ); } public function settings_page() { ?> } } ``` --- ```php function mypl_register_cpt() { register_post_type( 'book', array( 'labels' => 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' ); ``` ```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' ); ``` ```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' ); ?> } function mypl_save_meta( $post_id ) { // Security checks if ( ! isset( $_POST['mypl_meta_nonce'] ) || ! wp_verify_nonce( $_POST['mypl_meta_nonce'], 'mypl_save_meta' ) ) { return; } if ( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE ) { return; } if ( ! current_user_can( 'edit_post', $post_id ) ) { return; } // Save data if ( isset( $_POST['book_isbn'] ) ) { update_post_meta( $post_id, '_book_isbn', sanitize_text_field( $_POST['book_isbn'] ) ); } } add_action( 'save_post_book', 'mypl_save_meta' ); ``` ```php function mypl_add_menu() { add_options_page( 'My Plugin Settings', 'My Plugin', 'manage_options', 'my-plugin', 'mypl_settings_page' ); } add_action( 'admin_menu', 'mypl_add_menu' ); function mypl_register_settings() { register_setting( 'mypl_options', 'mypl_api_key', array( 'type' => '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' ); ?> } function mypl_settings_page() { if ( ! current_user_can( 'manage_options' ) ) { return; } ?> settings_fields( 'mypl_options' ); do_settings_sections( 'my-plugin' ); submit_button(); ?> } ``` ```php add_action( 'rest_api_init', function() { register_rest_route( 'myplugin/v1', '/data', array( 'methods' => 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 ); } ``` ```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); } }); }); }); ``` ```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' ); ``` ```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' ); ``` --- Use these production-ready templates to scaffold plugins quickly: When Claude should use these: When creating new plugins or implementing specific functionality patterns. 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 ``` Detailed documentation that Claude can load when needed: 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. --- ```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 ``` ```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' ); } ``` ```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' ); ``` ```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'] ); } } } ); ``` --- 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: Comprehensive Resources: Security Considerations: 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! --- Required: Optional: --- --- Solution: Solution: Flush rewrite rules ```php // Temporarily add to wp-admin flush_rewrite_rules(); // Remove after visiting wp-admin once ``` Solution: Solution: Solution: Use Solution: --- Use this checklist to verify your plugin: --- Questions? Issues?Issue #3: CSRF (Cross-Site Request Forgery)
wp_nonce_field() and wp_verify_nonce()Issue #4: Missing Capability Checks
is_admin() instead of current_user_can()Issue #5: Direct File Access
Issue #6: Prefix Collision
Issue #7: Rewrite Rules Not Flushed
Issue #8: Transients Not Cleaned
Issue #9: Scripts Loaded Everywhere
Issue #10: Missing Sanitization on Save
Issue #11: Incorrect LIKE Queries
$wpdb->esc_like()Issue #12: Using extract()
Issue #13: Missing Permission Callback in REST API
permission_callback specifiedIssue #14: Uninstall Hook Registered Repeatedly
Issue #15: Data Deleted on Deactivation
Issue #16: Using Deprecated Functions
Issue #17: Text Domain Mismatch
Issue #18: Missing Plugin Dependencies
Issue #19: Autosave Triggering Meta Save
Issue #20: admin-ajax.php Performance
Plugin Architecture Patterns
Pattern 1: Simple Plugin (Functions Only)
My Plugin Settings
Pattern 2: OOP Plugin
My Plugin Settings
Pattern 3: PSR-4 Plugin (Modern, Recommended)
My Plugin Settings
Common Patterns
Pattern 1: Custom Post Types
Pattern 2: Custom Taxonomies
Pattern 3: Meta Boxes
Pattern 4: Settings API
Pattern 5: REST API Endpoints
Pattern 6: AJAX Handlers (Legacy)
Pattern 7: Custom Database Tables
Pattern 8: Transients for Caching
Using Bundled Resources
Templates (templates/)
templates/plugin-simple/ - Simple plugin with functionstemplates/plugin-oop/ - Object-oriented plugin structuretemplates/plugin-psr4/ - Modern PSR-4 plugin with Composertemplates/examples/meta-box.php - Meta box implementationtemplates/examples/settings-page.php - Settings API pagetemplates/examples/custom-post-type.php - CPT registrationtemplates/examples/rest-endpoint.php - REST API endpointtemplates/examples/ajax-handler.php - AJAX implementationScripts (scripts/)
scripts/scaffold-plugin.sh - Interactive plugin scaffoldingscripts/check-security.sh - Security audit for common issuesscripts/validate-headers.sh - Verify plugin headersReferences (references/)
references/security-checklist.md - Complete security audit checklistreferences/hooks-reference.md - Common WordPress hooks and filtersreferences/sanitization-guide.md - All sanitization/escaping functionsreferences/wpdb-patterns.md - Database query patternsreferences/common-errors.md - Extended error prevention guideAdvanced Topics
Internationalization (i18n)
WP-CLI Commands
Scheduled Events (Cron)
Plugin Dependencies Check
Distribution & Auto-Updates
Enabling GitHub Auto-Updates
references/github-auto-updates.md (21 pages, all approaches)examples/github-updater.php (10 examples)Dependencies
Official Documentation
Troubleshooting
Problem: Plugin causes fatal error
Problem: 404 errors on custom post type pages
Problem: Nonce verification always fails
Problem: AJAX returns 0 or -1
wp_ajax_{action}Problem: Sanitization stripping HTML
wp_kses_post() instead of sanitize_text_field() to allow safe HTMLProblem: Database queries not working
$wpdb->prepare() for queries with variables$wpdb->prefixComplete Setup Checklist
references/common-errors.md for extended troubleshooting
More from this repository10
nextjs-shadcn-builder skill from ovachiever/droid-tings
security-auditor skill from ovachiever/droid-tings
threejs-graphics-optimizer skill from ovachiever/droid-tings
api-documenter skill from ovachiever/droid-tings
secret-scanner skill from ovachiever/droid-tings
applying-brand-guidelines skill from ovachiever/droid-tings
readme-updater skill from ovachiever/droid-tings
Configures Tailwind v4 with shadcn/ui, automating CSS variable setup, dark mode, and preventing common initialization errors.
dependency-auditor skill from ovachiever/droid-tings
deep-reading-analyst skill from ovachiever/droid-tings