Launch offer
Business website $150 USD Custom plugin $200 USD Ready in 5 days
Get a quote
WordPress Plugin Development

WordPress Plugin Development Complete Tutorial (95 Topics with Code)

Complete WordPress plugin development tutorial covering hooks, CPT, Settings API, REST, AJAX, WooCommerce, security, i18n, and wordpress.org release — with practical code examples.

This complete WordPress Plugin Development tutorial covers 95 topics — from your first plugin file to hooks, CPT, REST, AJAX, WooCommerce, security, i18n, and wordpress.org release. Beginner-friendly with production-minded code.

Course roadmap

1. Introduction to WordPress Plugin Development

WordPress plugins extend core features without editing WordPress itself. This series covers hooks, admin UI, CPT, REST, AJAX, security, WooCommerce, and shipping to WordPress.org.

  1. Learn plugin structure and hooks first.
  2. Build admin settings and secure forms.
  3. Ship a complete plugin with docs and updates.

Learning path

Basics + hooks → admin/settings/security
CPT/taxonomies/meta → shortcodes/blocks
REST/AJAX → WooCommerce/APIs
Security/i18n → wordpress.org release

2. What is a WordPress Plugin?

A plugin is a PHP (and JS/CSS) package that hooks into WordPress actions/filters to add features — themes change design; plugins change functionality.

Plugin vs theme

Theme  → look & layout
Plugin → features & behavior
Best practice: keep logic in plugins

3. WordPress Plugin Architecture

Typical architecture: main bootstrap file, includes for admin/public, assets, and hooks registered on load. Keep responsibilities separated.

Common layout

my-plugin/
  my-plugin.php          (bootstrap + header)
  includes/
  admin/
  public/
  assets/css|js
  languages/

4. Creating Your First WordPress Plugin

Put a PHP file with a valid plugin header inside `wp-content/plugins/your-plugin/`, then activate it in Plugins.

  1. Create `wp-content/plugins/hello-plugin/`.
  2. Add a main PHP file with a plugin header.
  3. Activate it in wp-admin → Plugins.

hello-plugin.php

<?php
/**
 * Plugin Name: Hello Plugin
 * Description: My first WordPress plugin.
 * Version: 1.0.0
 * Author: Imtiyaj
 */

add_action('init', function () {
    // Plugin loaded.
});

5. WordPress Plugin File Structure

Separate admin and front-end code, keep assets versioned, and autoload classes when the plugin gets larger.

Recommended structure

my-plugin/
  my-plugin.php
  uninstall.php
  includes/class-plugin.php
  admin/class-admin.php
  public/class-public.php
  assets/
  languages/

6. Plugin Header Information

WordPress reads the header comment to list your plugin. Include license, text domain, and requires versions for clarity.

Full header example

<?php
/**
 * Plugin Name: Acme Tools
 * Plugin URI: https://example.com/acme-tools
 * Description: Helpful tools for editors.
 * Version: 1.0.0
 * Requires at least: 6.0
 * Requires PHP: 7.4
 * Author: Acme
 * License: GPL-2.0-or-later
 * Text Domain: acme-tools
 * Domain Path: /languages
 */

7. Activating and Deactivating Plugins

Activation should set defaults and capabilities carefully. Deactivation should stop running features but usually keep data unless uninstalling.

Lifecycle note

Activate   → setup defaults, flush rewrites if needed
Deactivate → pause features, keep data
Uninstall  → remove options/tables (only if intended)

8. Plugin Activation and Deactivation Hooks

Use activation for DB/version setup. Use deactivation for cleanup like clearing cron events.

Hook registration

register_activation_hook(__FILE__, 'acme_activate');
register_deactivation_hook(__FILE__, 'acme_deactivate');

function acme_activate() {
    add_option('acme_version', '1.0.0');
    flush_rewrite_rules();
}

function acme_deactivate() {
    wp_clear_scheduled_hook('acme_daily_event');
    flush_rewrite_rules();
}

9. Uninstalling a Plugin

Only delete data on uninstall if that is expected. Check the uninstall constant and user capability.

uninstall.php

<?php
if (! defined('WP_UNINSTALL_PLUGIN')) {
    exit;
}

delete_option('acme_settings');
global $wpdb;
$wpdb->query("DROP TABLE IF EXISTS {$wpdb->prefix}acme_items");

10. WordPress Actions and Filters

Actions let you run code at moments in the WP lifecycle. Filters let you modify data before it is used/displayed.

Quick comparison

Action → do something (side effects)
Filter → change a value and return it

11. Understanding WordPress Hooks

Hooks are the core extension system. Priority controls order; accepted_args controls how many parameters your callback receives.

Hook basics

add_action('init', 'acme_boot', 10);
add_filter('the_title', 'acme_prefix_title', 10, 2);

function acme_prefix_title($title, $post_id) {
    return '[ACME] ' . $title;
}

12. Creating Custom Actions

Use `do_action()` at extension points. Document hook names and arguments for developers.

Custom action

// In your plugin:
do_action('acme_after_save_item', $item_id, $data);

// Elsewhere:
add_action('acme_after_save_item', function ($item_id, $data) {
    // react
}, 10, 2);

13. Creating Custom Filters

Always return a value from filter callbacks. Provide sensible defaults.

Custom filter

$label = apply_filters('acme_button_label', 'Save');

add_filter('acme_button_label', function ($label) {
    return 'Save changes';
});

14. WordPress Coding Standards

Use WordPress naming, Yoda-friendly comparisons where required by WPCS, proper escaping, and PHPCS with WordPress rulesets.

Style tips

- Prefix functions/classes/options
- Escape output, sanitize input
- Use strict comparisons when sensible
- Keep files focused and documented

15. Enqueue CSS and JavaScript

Never hardcode script tags in most cases. Enqueue with dependencies, versioning, and correct hooks (`wp_enqueue_scripts` / `admin_enqueue_scripts`).

Enqueue assets

add_action('wp_enqueue_scripts', function () {
    wp_enqueue_style('acme', plugins_url('assets/css/acme.css', __FILE__), [], '1.0.0');
    wp_enqueue_script('acme', plugins_url('assets/js/acme.js', __FILE__), ['jquery'], '1.0.0', true);
});

16. Adding Custom JavaScript

Localize AJAX URLs and nonces for front-end scripts. Keep JS modular as features grow.

Localize script

wp_localize_script('acme', 'AcmeData', [
    'ajaxUrl' => admin_url('admin-ajax.php'),
    'nonce'   => wp_create_nonce('acme_ajax'),
]);

17. Adding Custom CSS

Prefix CSS classes with your plugin slug. Load admin CSS only on your plugin screens when possible.

Admin-only CSS

add_action('admin_enqueue_scripts', function ($hook) {
    if ($hook !== 'toplevel_page_acme-settings') {
        return;
    }
    wp_enqueue_style('acme-admin', plugins_url('assets/css/admin.css', __FILE__), [], '1.0.0');
});

18. WordPress Admin Menu

Use `add_menu_page` / `add_submenu_page` with capability checks so only allowed users see the screens.

Admin menu

add_action('admin_menu', function () {
    add_menu_page(
        'Acme Settings',
        'Acme',
        'manage_options',
        'acme-settings',
        'acme_render_settings',
        'dashicons-admin-generic',
        58
    );
});

19. Creating Admin Settings Pages

Render forms with Settings API fields, show success notices, and keep markup accessible.

Render callback sketch

function acme_render_settings() {
    if (! current_user_can('manage_options')) {
        return;
    }
    echo '<div class="wrap"><h1>Acme Settings</h1>';
    echo '<form method="post" action="options.php">';
    settings_fields('acme_settings_group');
    do_settings_sections('acme-settings');
    submit_button();
    echo '</form></div>';
}

20. WordPress Settings API

Settings API handles nonces, capabilities, and option saving patterns more safely than raw forms.

Register setting

add_action('admin_init', function () {
    register_setting('acme_settings_group', 'acme_settings', [
        'type' => 'array',
        'sanitize_callback' => 'acme_sanitize_settings',
        'default' => [],
    ]);
});

21. WordPress Options API

Prefer a single array option for related settings. Autoload only what is needed on every request.

Options usage

$settings = get_option('acme_settings', []);
$settings['api_key'] = 'xxx';
update_option('acme_settings', $settings, false);

22. Creating Custom Admin Forms

For non-Settings-API forms, verify nonce + capability, sanitize input, then redirect with a query arg notice.

Handle POST

if (isset($_POST['acme_save'])) {
    check_admin_referer('acme_save_action');
    if (! current_user_can('manage_options')) {
        wp_die('Forbidden');
    }
    $title = sanitize_text_field(wp_unslash($_POST['title'] ?? ''));
    // save...
    wp_safe_redirect(add_query_arg('updated', '1'));
    exit;
}

23. Nonces and Security in WordPress Plugins

Nonces help verify intent/origin for forms and AJAX. Always pair with capability checks — nonces are not permissions.

Create and verify

wp_nonce_field('acme_save_action');
// ...
check_admin_referer('acme_save_action');
// AJAX:
check_ajax_referer('acme_ajax', 'nonce');

24. User Capabilities and Permissions

Check capabilities before rendering UI and before processing actions. Map custom caps to roles on activation if needed.

Capability check

if (! current_user_can('manage_options')) {
    wp_die(esc_html__('You do not have permission.', 'acme-tools'));
}

25. Sanitization and Validation

Sanitize cleans data. Validate checks business rules. Do both on input; escape on output.

Sanitize helpers

$email = sanitize_email(wp_unslash($_POST['email'] ?? ''));
$url   = esc_url_raw(wp_unslash($_POST['url'] ?? ''));
$html  = wp_kses_post(wp_unslash($_POST['content'] ?? ''));
if (! is_email($email)) {
    // invalid
}

26. Escaping WordPress Data

Escape late (when printing). Choose the right esc_* helper for the context.

Escape examples

echo esc_html($title);
echo '<a href="' . esc_url($link) . '">' . esc_html($label) . '</a>';
echo '<input value="' . esc_attr($value) . '">';

27. WordPress Database Structure

Understanding core schema helps you decide between post meta, custom tables, or taxonomies.

Core tables (common)

wp_posts / wp_postmeta
wp_users / wp_usermeta
wp_options
wp_terms / wp_term_taxonomy / wp_term_relationships

28. The $wpdb Class

Use `$wpdb->prepare()` for dynamic SQL. Prefer WP APIs when they fit; use `$wpdb` for custom tables/queries.

Prepared query

global $wpdb;
$row = $wpdb->get_row(
    $wpdb->prepare("SELECT * FROM {$wpdb->prefix}acme_items WHERE id = %d", $id)
);

29. Creating Custom Database Tables

Use `dbDelta` carefully (exact formatting matters). Store a DB version option for upgrades.

dbDelta create

global $wpdb;
$table = $wpdb->prefix . 'acme_items';
$charset = $wpdb->get_charset_collate();
$sql = "CREATE TABLE $table (
  id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
  title VARCHAR(200) NOT NULL,
  created_at DATETIME NOT NULL,
  PRIMARY KEY  (id)
) $charset;";
require_once ABSPATH . 'wp-admin/includes/upgrade.php';
dbDelta($sql);

30. CRUD Operations with $wpdb

`insert`, `update`, `delete`, and `get_*` methods cover most custom-table CRUD needs.

CRUD helpers

global $wpdb;
$table = $wpdb->prefix . 'acme_items';
$wpdb->insert($table, ['title' => 'Hello', 'created_at' => current_time('mysql')], ['%s','%s']);
$wpdb->update($table, ['title' => 'Hi'], ['id' => 1], ['%s'], ['%d']);
$wpdb->delete($table, ['id' => 1], ['%d']);

31. Database Migrations and Updates

Compare stored DB version to plugin DB version on `plugins_loaded`/`admin_init`, then migrate and update the option.

Versioned upgrade

define('ACME_DB_VERSION', '1.1.0');
add_action('plugins_loaded', function () {
    if (get_option('acme_db_version') === ACME_DB_VERSION) {
        return;
    }
    // run migrations...
    update_option('acme_db_version', ACME_DB_VERSION);
});

32. Custom Post Types

CPTs power portfolios, products, FAQs, and more. Set labels, supports, and rewrite carefully.

Register CPT

add_action('init', function () {
    register_post_type('book', [
        'label' => 'Books',
        'public' => true,
        'show_in_rest' => true,
        'supports' => ['title', 'editor', 'thumbnail'],
        'has_archive' => true,
    ]);
});

33. Custom Taxonomies

Taxonomies can be hierarchical (like categories) or flat (like tags).

Register taxonomy

add_action('init', function () {
    register_taxonomy('genre', 'book', [
        'label' => 'Genres',
        'hierarchical' => true,
        'show_in_rest' => true,
        'public' => true,
    ]);
});

34. Custom Fields in WordPress Plugins

Custom fields store values in postmeta. Prefer uniquely prefixed meta keys.

Save meta

update_post_meta($post_id, '_acme_isbn', sanitize_text_field($isbn));
$isbn = get_post_meta($post_id, '_acme_isbn', true);

35. Meta Boxes in WordPress

Register meta boxes, render fields, and save on `save_post` with nonce + capability checks.

Meta box sketch

add_action('add_meta_boxes', function () {
    add_meta_box('acme_book', 'Book Details', 'acme_book_box', 'book');
});

add_action('save_post_book', function ($post_id) {
    if (! isset($_POST['acme_book_nonce']) || ! wp_verify_nonce(sanitize_text_field(wp_unslash($_POST['acme_book_nonce'])), 'acme_book_save')) {
        return;
    }
    // save fields...
});

36. Post Meta and User Meta

Use post meta for content attributes and user meta for per-user preferences/state.

User meta

update_user_meta($user_id, 'acme_timezone', 'Asia/Kolkata');
$tz = get_user_meta($user_id, 'acme_timezone', true);

37. Shortcodes in WordPress

Shortcodes are still useful for classic content and some builder workflows. Prefer blocks for new Gutenberg UIs when possible.

Shortcode idea

[acme_books limit="5"] → renders a book list

38. Creating Custom Shortcodes

Always return output (don’t echo). Escape values and sanitize attributes.

add_shortcode

add_shortcode('acme_greeting', function ($atts) {
    $atts = shortcode_atts(['name' => 'friend'], $atts, 'acme_greeting');
    return '<p>Hello, ' . esc_html($atts['name']) . '!</p>';
});

39. Widgets in WordPress Plugins

Widgets remain relevant for classic themes. Block themes often use block-based alternatives.

Widget sketch

class Acme_Widget extends WP_Widget {
    public function __construct() {
        parent::__construct('acme_widget', 'Acme Widget');
    }
    public function widget($args, $instance) {
        echo $args['before_widget'];
        echo esc_html($instance['title'] ?? 'Acme');
        echo $args['after_widget'];
    }
}
add_action('widgets_init', fn () => register_widget('Acme_Widget'));

40. Gutenberg Block Development

Blocks are the preferred content UI. Use `@wordpress/scripts` for a modern JS build.

Block stack

block.json + edit.js + save.js/render.php
@wordpress/scripts for bundling
register_block_type in PHP

41. Creating Custom Gutenberg Blocks

Start with a simple static block, then add attributes and controls.

Register block

add_action('init', function () {
    register_block_type(__DIR__ . '/blocks/notice');
});

42. Dynamic Gutenberg Blocks

Dynamic blocks use a `render_callback` / `render.php` so output stays fresh from the database.

Render callback idea

register_block_type(__DIR__ . '/blocks/books', [
    'render_callback' => function ($attributes) {
        return '<div class="acme-books">...</div>';
    },
]);

43. WordPress REST API

Core exposes posts, users, and more under `/wp-json/`. Plugins can add custom namespaces/routes.

Core endpoint example

GET /wp-json/wp/v2/posts

44. Creating Custom REST API Endpoints

Define namespace, route, methods, permission_callback, and args schema.

Custom route

add_action('rest_api_init', function () {
    register_rest_route('acme/v1', '/items', [
        'methods' => 'GET',
        'callback' => 'acme_rest_items',
        'permission_callback' => '__return_true',
    ]);
});

45. REST API Authentication

For same-origin wp-admin/editor requests, cookie + nonce works. For external clients, use application passwords or OAuth/JWT solutions.

Permission callback

'permission_callback' => function () {
    return current_user_can('edit_posts');
}

46. AJAX in WordPress

WordPress AJAX uses action names hooked to `wp_ajax_{action}` and optionally `wp_ajax_nopriv_{action}`.

AJAX flow

JS → admin-ajax.php?action=acme_save
PHP hook → verify nonce/caps → JSON response

47. wp_ajax and wp_ajax_nopriv

`wp_ajax_` is for logged-in users. `wp_ajax_nopriv_` is for guests. Add both only when needed.

Register handlers

add_action('wp_ajax_acme_save', 'acme_ajax_save');
add_action('wp_ajax_nopriv_acme_save', 'acme_ajax_save');

48. Form Submission with AJAX

Send nonce + fields, verify on server, then `wp_send_json_success` / `wp_send_json_error`.

Handler sketch

function acme_ajax_save() {
    check_ajax_referer('acme_ajax', 'nonce');
    $title = sanitize_text_field(wp_unslash($_POST['title'] ?? ''));
    if ($title === '') {
        wp_send_json_error(['message' => 'Title required'], 400);
    }
    wp_send_json_success(['message' => 'Saved']);
}

49. WordPress Forms and Validation

Never trust client-only validation. Return clear field errors and preserve safe old input.

Validation pattern

$errors = [];
if ($email === '' || ! is_email($email)) {
    $errors['email'] = 'Enter a valid email';
}
if ($errors) {
    // show errors
}

50. File Upload Handling in Plugins

Check capabilities, nonces, and allowed mime types. Prefer Media Library APIs when files should be attachments.

Upload sketch

require_once ABSPATH . 'wp-admin/includes/file.php';
$file = wp_handle_upload($_FILES['acme_file'], ['test_form' => false]);
if (isset($file['error'])) {
    // handle error
}

51. Media Library Integration

Use `media_handle_upload` or sideload helpers so files become attachments with metadata.

media_handle_upload

require_once ABSPATH . 'wp-admin/includes/image.php';
require_once ABSPATH . 'wp-admin/includes/file.php';
require_once ABSPATH . 'wp-admin/includes/media.php';
$attachment_id = media_handle_upload('acme_file', 0);

52. Cron Jobs / WP-Cron

WP-Cron runs on page loads by default. For critical schedules, use real server cron triggering `wp-cron.php`.

Schedule event

if (! wp_next_scheduled('acme_daily_event')) {
    wp_schedule_event(time(), 'daily', 'acme_daily_event');
}
add_action('acme_daily_event', function () {
    // daily task
});

53. Sending Emails with wp_mail()

`wp_mail()` is the WP wrapper around PHPMailer. Set headers for HTML/from address carefully.

wp_mail example

wp_mail(
    'user@example.com',
    'Welcome',
    'Thanks for joining!',
    ['Content-Type: text/plain; charset=UTF-8']
);

54. SMTP Integration for WordPress Plugins

Hook `phpmailer_init` or integrate with an SMTP plugin/API provider. Keep credentials in protected options/env.

phpmailer_init sketch

add_action('phpmailer_init', function ($phpmailer) {
    $phpmailer->isSMTP();
    $phpmailer->Host = 'smtp.example.com';
    $phpmailer->SMTPAuth = true;
    $phpmailer->Port = 587;
    $phpmailer->Username = 'user';
    $phpmailer->Password = 'pass';
});

55. User Registration and Login in Plugins

Prefer core auth APIs. If building custom forms, use nonces, strong password handling, and `wp_signon` carefully.

Create user

$user_id = wp_insert_user([
    'user_login' => $login,
    'user_email' => $email,
    'user_pass'  => $password,
    'role'       => 'subscriber',
]);

56. User Roles and Capabilities

Use `add_role` / `get_role()->add_cap()` on activation and remove carefully on uninstall if appropriate.

Add capability

$role = get_role('editor');
if ($role) {
    $role->add_cap('manage_acme_items');
}

57. WooCommerce Plugin Integration

Guard WooCommerce-dependent code. Hook into product/order lifecycle instead of editing WooCommerce core.

Dependency check

add_action('plugins_loaded', function () {
    if (! class_exists('WooCommerce')) {
        return;
    }
    // load Woo features
});

58. WooCommerce Hooks and Filters

WooCommerce is hook-rich. Prefer filters/actions over template overrides when possible.

Example filter

add_filter('woocommerce_product_get_price', function ($price, $product) {
    return $price;
}, 10, 2);

59. Payment Gateway Plugin Development

Extend `WC_Payment_Gateway`, implement form fields/process_payment, and verify webhooks server-side.

Gateway checklist

1. Extend WC_Payment_Gateway
2. Register via woocommerce_payment_gateways
3. process_payment + thank you page
4. Webhook signature verification
5. Order notes + status updates

60. Third-Party API Integration

Store API keys encrypted/protected when possible, handle timeouts/errors, and never expose secrets to the browser.

Integration tips

- Keys in options/env, not JS
- Timeouts + error logging
- Retry carefully for idempotent GETs
- Cache responses when useful

61. Webhooks Integration

Expose a REST route or admin-post endpoint, verify signatures, and process quickly (queue heavy work).

Webhook route idea

register_rest_route('acme/v1', '/webhook', [
    'methods' => 'POST',
    'callback' => 'acme_handle_webhook',
    'permission_callback' => '__return_true',
]);

62. OAuth Authentication in WordPress Plugins

Store client IDs/secrets safely, validate state parameters, and map provider profile emails to WP users carefully.

OAuth steps

1. Redirect to provider
2. Receive code + validate state
3. Exchange code for tokens
4. Fetch profile
5. Create/login WP user

63. API Requests with wp_remote_get()

`wp_remote_get` is the WP-native way to call APIs without requiring curl extensions directly.

wp_remote_get

$response = wp_remote_get('https://api.example.com/data', [
    'timeout' => 15,
    'headers' => ['Authorization' => 'Bearer ' . $token],
]);
if (is_wp_error($response)) {
    return $response;
}
$data = json_decode(wp_remote_retrieve_body($response), true);

64. API Requests with wp_remote_post()

Use JSON body + content-type headers for modern APIs. Always check for `WP_Error` and status codes.

wp_remote_post JSON

$response = wp_remote_post('https://api.example.com/items', [
    'timeout' => 15,
    'headers' => ['Content-Type' => 'application/json'],
    'body' => wp_json_encode(['title' => 'Hello']),
]);

65. Plugin Settings Import/Export

Validate imported JSON schema, check capabilities/nonces, and avoid executing imported PHP.

Export JSON

$settings = get_option('acme_settings', []);
header('Content-Type: application/json');
echo wp_json_encode($settings);
exit;

66. Plugin Localization and Translation

Wrap UI strings with translation functions and load `.mo` files on `init`/`plugins_loaded`.

Load text domain

add_action('init', function () {
    load_plugin_textdomain('acme-tools', false, dirname(plugin_basename(__FILE__)) . '/languages');
});
echo esc_html__('Settings saved.', 'acme-tools');

67. Internationalization (i18n)

Internationalization prepares strings for translation. Localization applies a language.

i18n helpers

esc_html_e('Dashboard', 'acme-tools');
printf(
    esc_html(_n('%d item', '%d items', $count, 'acme-tools')),
    (int) $count
);

68. Multilingual Plugin Compatibility

Use language-aware queries where needed, translate strings via text domains, and avoid hardcoding language URLs.

Compatibility tips

- Translate all UI strings
- Store language meta when needed
- Test permalinks per language
- Avoid concatenating sentences for translation

69. Plugin Performance Optimization

Load code only when needed. Avoid unbounded queries. Profile with Query Monitor.

Perf checklist

- Conditional asset loading
- Cache remote API responses
- Avoid N+1 queries
- Autoload options carefully
- Lazy-load admin-only code

70. WordPress Caching for Plugins

Transients work everywhere; object cache shines with Redis/Memcached. Set sensible TTLs and invalidate on updates.

Transient example

$data = get_transient('acme_remote_data');
if (false === $data) {
    $data = acme_fetch_remote();
    set_transient('acme_remote_data', $data, HOUR_IN_SECONDS);
}

71. Plugin Security Best Practices

Security is not one function — combine authorization, authentication intent checks, input hygiene, and safe output.

Security pillars

1. Capability checks
2. Nonces for state changes
3. Sanitize/validate input
4. Escape output
5. Prepared SQL
6. Least privilege

72. Preventing CSRF in WordPress Plugins

CSRF tricks a logged-in browser into submitting requests. Nonces + same-site practices reduce this risk.

CSRF protection

check_admin_referer('acme_update');
// or for GET actions:
wp_verify_nonce(sanitize_text_field(wp_unslash($_GET['_wpnonce'] ?? '')), 'acme_delete');

73. Preventing SQL Injection

Never concatenate unsanitized user input into SQL. Prepare values; allowlist column/orderby names.

Safe query

$rows = $wpdb->get_results(
    $wpdb->prepare("SELECT * FROM {$wpdb->prefix}acme_items WHERE title LIKE %s", '%' . $wpdb->esc_like($q) . '%')
);

74. Preventing XSS in WordPress Plugins

XSS injects hostile scripts into pages. Escape by context and use `wp_kses` when HTML is required.

Safe HTML output

echo wp_kses_post($html);
echo esc_html($text);
echo esc_js($js_string);

75. Preventing Unauthorized Access

Check capabilities everywhere it matters. Guard PHP files with `ABSPATH` checks.

Direct access guard

<?php
if (! defined('ABSPATH')) {
    exit;
}

76. WordPress Debugging for Plugin Developers

Use `WP_DEBUG`, `WP_DEBUG_LOG`, and Query Monitor. Fix notices before release.

wp-config debug

define('WP_DEBUG', true);
define('WP_DEBUG_LOG', true);
define('WP_DEBUG_DISPLAY', false);

77. Error Handling and Logging

Catch expected failures, log context for yourself, and show safe messages to users.

Log error

if (is_wp_error($response)) {
    error_log('Acme API failed: ' . $response->get_error_message());
}

78. Plugin Compatibility Testing

Compatibility issues often come from CSS conflicts, jQuery assumptions, or aggressive global hooks.

Test matrix idea

WP latest + previous
PHP supported versions
WooCommerce on/off
Block theme + classic theme
Caching plugin enabled

79. WordPress Version Compatibility

Use “Requires at least” accurately. Avoid relying on functions that do not exist in your minimum version without guards.

Function exists guard

if (function_exists('wp_get_theme')) {
    // safe to call
}

80. PHP Version Compatibility

Declare `Requires PHP` in the header. Test on the oldest PHP you claim to support.

Header field

 * Requires PHP: 7.4

81. Unit Testing WordPress Plugins

Tests protect against regressions in sanitize logic, REST permissions, and helpers.

Test mindset

Unit test pure helpers
Integration test CPT registration / REST routes
Run in CI on push

82. Plugin Documentation

Good docs reduce support load. Include screenshots, requirements, and changelog entries.

Docs checklist

README / help tab
Hook reference for developers
Changelog
Support/contact path

83. Creating Free and Pro Plugin Versions

Common patterns: free core plugin + pro add-on, or feature flags unlocked by license. Keep free valuable.

Architecture options

A) Free plugin + Pro extension plugin
B) One plugin with license-gated features
Prefer clear UX and fair free tier

84. Plugin Licensing System

Verify licenses server-side, cache status with expiry, and fail gracefully when offline.

License check idea

$response = wp_remote_post('https://example.com/license/activate', [
    'body' => ['license' => $key, 'site' => home_url()],
]);

85. Plugin Update Mechanism

WordPress.org handles updates for hosted plugins. Premium plugins use custom update APIs via transient filters.

Update sources

wordpress.org SVN hosting
or
custom updater (EDD, Freemius, self-hosted API)

86. Automatic Plugin Updates

Follow semver. Use automatic updates responsibly; major breaks should be rare and documented.

Auto-update filter tip

add_filter('auto_update_plugin', function ($update, $item) {
    if (isset($item->slug) && $item->slug === 'acme-tools') {
        return true;
    }
    return $update;
}, 10, 2);

87. Preparing a Plugin for WordPress.org

Follow review guidelines, remove tracking violations, and ensure GPL-compatible licensing.

Prep checklist

- GPL-compatible license
- No obfuscated code
- Proper prefixes
- readme.txt valid
- Security basics in place

88. readme.txt and Plugin Assets

readme.txt powers the wordpress.org plugin page. Assets go in the `assets/` directory in SVN (not the plugin zip root necessarily).

readme.txt sections

Stable tag
Requires at least / Tested up to / Requires PHP
Description
Installation
FAQ
Changelog
Upgrade Notice

89. WordPress.org Plugin Review Guidelines

Avoid phone-home abuse, loading remote PHP, and trademark issues. Keep permissions honest.

Common rejection causes

- Calling home without disclosure
- Including unused/bundled libraries poorly
- Security issues (CSRF/XSS/SQLi)
- Misleading naming/branding

90. SVN and WordPress.org Plugin Deployment

Develop in Git if you want, then deploy to wordpress.org SVN (`trunk` + `tags/x.y.z`).

SVN release flow

1. svn co plugin repo
2. update trunk
3. svn cp trunk tags/1.0.1
4. svn ci -m "Release 1.0.1"

91. Git and Plugin Development Workflow

Keep main releasable. Use feature branches, semantic commits, and CI checks before merge.

Git workflow

main ← release
feature/* ← PRs
tag v1.2.0 ← releases
CI: phpcs + tests

92. Plugin Release and Version Management

Bump version in plugin header, readme stable tag, and package metadata together.

Release checklist

1. Bump version
2. Update changelog
3. Test upgrade path
4. Tag release
5. Deploy (SVN/custom updater)

93. Building a Complete WordPress Plugin

Plan features, build MVP hooks/CPT/settings, then harden security, i18n, and docs before release.

Build order

1. Bootstrap + settings
2. CPT/UI or core feature
3. Security pass
4. i18n + readme
5. QA matrix
6. Release

94. WordPress Plugin Development Interview Questions

Be ready to explain actions vs filters, nonces vs capabilities, and when to use custom tables.

Sample Q&A

Q: Action vs filter?
A: Actions run events; filters modify values.

Q: Are nonces permissions?
A: No — still check capabilities.

Q: Escape where?
A: On output, by context.

Q: When custom table?
A: High-volume/relational data unfit for postmeta.

95. Final Project – Complete Professional WordPress Plugin

Build one complete plugin end-to-end — activation, admin settings, a main feature (CPT or custom table), REST or AJAX, security hardening, translations, and release-ready readme.

  1. Define the feature MVP.
  2. Implement secure admin + data layer.
  3. Add REST/AJAX integration.
  4. Finish i18n, docs, and release checklist.

Final project scope

1. Plugin bootstrap + activation/uninstall
2. Settings API page
3. CPT + meta box OR custom table CRUD
4. Shortcode or Gutenberg block
5. REST endpoint and/or AJAX form
6. Nonces, caps, sanitize/escape
7. i18n text domain
8. readme.txt + changelog
9. Basic PHPUnit or manual test checklist

Starter bootstrap

<?php
/**
 * Plugin Name: Acme Library
 * Version: 1.0.0
 * Text Domain: acme-library
 */

if (! defined('ABSPATH')) {
    exit;
}

define('ACME_LIBRARY_VERSION', '1.0.0');
require_once __DIR__ . '/includes/class-acme-library.php';
AcmeLibrary::instance(__FILE__);

Conclusion

You now have a professional WordPress plugin path: architecture, hooks, admin settings, data storage, blocks/REST/AJAX, WooCommerce extensions, security, translations, and release workflows. Finish the final project to turn the lessons into a shippable plugin.

Leave a reply

Your email address will not be published. Required fields are marked *