🎯

wordpress-plugin-core

🎯Skill

from ovachiever/droid-tings

VibeIndex|
What it does

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)

wordpress-plugin-core

Installation

git cloneClone repository
git clone https://github.com/ovachiever/droid-tings.git
πŸ“– Extracted from docs: ovachiever/droid-tings
14Installs
-
AddedFeb 4, 2026

Skill Details

SKILL.md

|

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 - 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

)

);

```

---

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 '

';

```

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

/**

* 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() {

?>

}

```

Pattern 2: OOP Plugin

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();

```

Pattern 3: PSR-4 Plugin (Modern, Recommended)

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() {

?>

}

}

```

---

Common Patterns

Pattern 1: Custom Post Types

```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' );

```

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' );

?>

}

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' );

```

Pattern 4: Settings API

```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();

?>

}

```

Pattern 5: REST API Endpoints

```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 );

}

```

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:

When Claude should use these: When creating new plugins or implementing specific functionality patterns.

Scripts (scripts/)

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:

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 '

';

} );

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:

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!

---