iwantcoding.com
🔥 Daily 👥 Rooms 🏆 Top Log in Sign up

Plugins

A plugin extends WordPress without touching core. One folder + one PHP file with a header = a plugin. Use hooks (add_action / add_filter) to wire it into the loading lifecycle.

Anatomy, hooks, settings, security

EXAMPLE
<?php
/*
 * Plugin Name:       My Plugin
 * Plugin URI:        https://example.com/my-plugin
 * Description:       Adds a custom feature.
 * Version:           1.0.0
 * Requires at least: 6.5
 * Requires PHP:      8.1
 * Author:            Ada Lovelace
 * License:           GPL-2.0-or-later
 * Text Domain:       my-plugin
 */

// 1) Prevent direct access
if (!defined('ABSPATH')) exit;

// 2) Plugin constants
define('MY_PLUGIN_VERSION', '1.0.0');
define('MY_PLUGIN_DIR',     plugin_dir_path(__FILE__));
define('MY_PLUGIN_URL',     plugin_dir_url(__FILE__));

// 3) Activation / deactivation / uninstall hooks
register_activation_hook(__FILE__,   'my_plugin_activate');
register_deactivation_hook(__FILE__, 'my_plugin_deactivate');
register_uninstall_hook(__FILE__,    'my_plugin_uninstall');

function my_plugin_activate() {
    // create custom DB tables, options, schedule cron, flush rewrite rules
    add_option('my_plugin_options', ['enabled' => true, 'count' => 0]);
    flush_rewrite_rules();
}

function my_plugin_deactivate() {
    flush_rewrite_rules();
    wp_clear_scheduled_hook('my_plugin_cron');
}

function my_plugin_uninstall() {
    // remove all plugin data (only when uninstalled via admin UI)
    delete_option('my_plugin_options');
    // Drop custom tables, etc.
}

// 4) Internationalisation
add_action('init', function () {
    load_plugin_textdomain('my-plugin', false, dirname(plugin_basename(__FILE__)) . '/languages');
});

// 5) Enqueue assets — front-end + admin
add_action('wp_enqueue_scripts', function () {
    wp_enqueue_style(
        'my-plugin-front',
        MY_PLUGIN_URL . 'assets/css/front.css',
        [],
        MY_PLUGIN_VERSION,
    );
    wp_enqueue_script(
        'my-plugin-front',
        MY_PLUGIN_URL . 'assets/js/front.js',
        [],
        MY_PLUGIN_VERSION,
        true,
    );
    // Pass server data to JS
    wp_localize_script('my-plugin-front', 'MyPluginData', [
        'ajaxUrl' => admin_url('admin-ajax.php'),
        'nonce'   => wp_create_nonce('my_plugin'),
    ]);
});

add_action('admin_enqueue_scripts', function ($hook) {
    if ($hook !== 'settings_page_my-plugin') return;     // only on our settings page
    wp_enqueue_style('my-plugin-admin', MY_PLUGIN_URL . 'assets/css/admin.css', [], MY_PLUGIN_VERSION);
});

// 6) Register a Custom Post Type via plugin
add_action('init', function () {
    register_post_type('my_item', [
        'public'        => true,
        'label'         => 'My Items',
        'supports'      => ['title', 'editor', 'thumbnail', 'excerpt'],
        'show_in_rest'  => true,
        'menu_icon'     => 'dashicons-cart',
        'rewrite'       => ['slug' => 'items'],
    ]);
});

// 7) Shortcode
add_shortcode('my_button', function ($atts) {
    $atts = shortcode_atts([
        'href' => '#',
        'label' => 'Click me',
        'style' => 'primary',
    ], $atts);
    return sprintf(
        '<a class="my-btn my-btn--%s" href="%s">%s</a>',
        esc_attr($atts['style']),
        esc_url($atts['href']),
        esc_html($atts['label']),
    );
});

// 8) Settings page (Settings API)
add_action('admin_menu', function () {
    add_options_page(
        'My Plugin',
        'My Plugin',
        'manage_options',
        'my-plugin',
        'my_plugin_render_settings',
    );
});

add_action('admin_init', function () {
    register_setting('my_plugin_group', 'my_plugin_options', [
        'type'              => 'array',
        'sanitize_callback' => 'my_plugin_sanitize',
        'default'           => ['enabled' => true, 'count' => 0],
    ]);
    add_settings_section('main', 'General', null, 'my-plugin');
    add_settings_field('enabled', 'Enabled', function () {
        $opts = get_option('my_plugin_options');
        printf('<input type="checkbox" name="my_plugin_options[enabled]" %s />',
            checked(!empty($opts['enabled']), true, false));
    }, 'my-plugin', 'main');
});

function my_plugin_sanitize($input) {
    return [
        'enabled' => !empty($input['enabled']),
        'count'   => max(0, (int) ($input['count'] ?? 0)),
    ];
}

function my_plugin_render_settings() {
    if (!current_user_can('manage_options')) return;
    ?>
    <div class="wrap">
        <h1>My Plugin</h1>
        <form method="post" action="options.php">
            <?php
            settings_fields('my_plugin_group');
            do_settings_sections('my-plugin');
            submit_button();
            ?>
        </form>
    </div>
    <?php
}

// 9) REST API endpoint
add_action('rest_api_init', function () {
    register_rest_route('my-plugin/v1', '/items', [
        'methods'             => 'GET',
        'callback'            => 'my_plugin_get_items',
        'permission_callback' => '__return_true',
        'args' => [
            'count' => [
                'default'           => 10,
                'sanitize_callback' => 'absint',
                'validate_callback' => fn($v) => $v >= 1 && $v <= 100,
            ],
        ],
    ]);
});

function my_plugin_get_items(WP_REST_Request $req) {
    $query = new WP_Query([
        'post_type'      => 'my_item',
        'posts_per_page' => $req->get_param('count'),
    ]);
    $out = [];
    while ($query->have_posts()) {
        $query->the_post();
        $out[] = [
            'id'    => get_the_ID(),
            'title' => get_the_title(),
            'url'   => get_permalink(),
        ];
    }
    wp_reset_postdata();
    return rest_ensure_response($out);
}

// 10) AJAX endpoint (legacy admin-ajax.php)
add_action('wp_ajax_my_plugin_count',         'my_plugin_ajax_count');
add_action('wp_ajax_nopriv_my_plugin_count',  'my_plugin_ajax_count');

function my_plugin_ajax_count() {
    check_ajax_referer('my_plugin', 'nonce');
    $opts = get_option('my_plugin_options');
    $opts['count']++;
    update_option('my_plugin_options', $opts);
    wp_send_json_success(['count' => $opts['count']]);
}

// 11) Cron
add_action('my_plugin_cron', 'my_plugin_cron_task');

add_action('wp', function () {
    if (!wp_next_scheduled('my_plugin_cron')) {
        wp_schedule_event(time(), 'daily', 'my_plugin_cron');
    }
});

function my_plugin_cron_task() {
    // daily work — clean up old data, send digest emails, etc.
}

// 12) Security checklist
//   ✅ Check capabilities before privileged actions: current_user_can('manage_options')
//   ✅ Use nonces for state-changing forms / AJAX: wp_create_nonce + check_admin_referer
//   ✅ Sanitize input: sanitize_text_field, absint, esc_url_raw, wp_kses_post
//   ✅ Escape output: esc_html, esc_attr, esc_url, wp_kses_post
//   ✅ Use prepared statements: $wpdb->prepare
//   ✅ Avoid direct $_POST / $_GET — use the REST request param API or sanitize
//   ✅ Validate REST args via sanitize_callback + validate_callback
//   ✅ ABSPATH check at top of every file
//   ✅ Plugin uninstall: provide uninstall.php to remove options + tables

// 13) Plugin folder structure (recommended)
// my-plugin/
//   my-plugin.php          (main file with the plugin header)
//   uninstall.php
//   readme.txt             (WordPress.org format)
//   readme.md
//   LICENSE
//   languages/             (translation .pot, .po, .mo)
//   assets/
//       css/
//       js/
//       img/
//   includes/              (PHP classes / modules)
//       class-my-plugin.php
//       class-rest.php
//       class-admin.php
//   templates/             (override-able templates)

// 14) PSR-4 autoload (modern plugins use Composer)
// composer.json:
//   {
//       "name": "my-vendor/my-plugin",
//       "require": { "php": ">=8.1" },
//       "autoload": { "psr-4": { "MyPlugin\\\\": "includes/" } }
//   }
// Then:
//   require_once MY_PLUGIN_DIR . 'vendor/autoload.php';
//   use MyPlugin\\Rest;
//   (new Rest())->register();

// 15) Plugin best practices
//   • Pin Requires at least + Requires PHP — prevents activation on incompatible sites
//   • Use prefixed function / class names (my_plugin_*) to avoid collisions
//   • Don't fire actions on every request — use cron / events
//   • Bail early if not an admin / not the right hook — performance
//   • Provide an uninstall.php — cleanly remove data when uninstalled
//   • Internationalise from the start — load_plugin_textdomain + __() / _e()
//   • Read OTHER plugins' hook docs before overriding their behaviour
//   • Auto-update gracefully — Plugin Update Checker for paid plugins; WordPress.org SVN for free

// 16) Distribution
//   • Free: WordPress.org plugin repo (SVN-based release; review takes 1-4 weeks)
//   • Paid: own site + license check; auto-update via Plugin Update Checker library
//   • Internal: a custom updater that fetches from a private endpoint

Why it matters

A plugin is just one PHP file with a comment header — everything else is hooks. Start tiny; add a settings page only when you need configurability; reach for classes + Composer autoload when the plugin grows past a few files.

Tip: Tweak the snippet with Try it Yourself », then sit the quiz at the bottom of the page.

Example

Example
// Plugin = feature add-on.
// Activate via /wp-admin > Plugins.
Try it Yourself »

Discussion

Loading…