Skip to content

Latest commit

 

History

History
490 lines (397 loc) · 12.6 KB

File metadata and controls

490 lines (397 loc) · 12.6 KB

Part 3 – Becoming a Plugin Developer

Introduction to Plugin Development

Structure of a Plugin

A WordPress plugin consists of several essential files:

Main Plugin File (plugin.php)

<?php
/**
 * Plugin Name: My Custom Plugin
 * Plugin URI: https://example.com/my-plugin
 * Description: A custom WordPress plugin for professional websites
 * Version: 1.0.0
 * Author: Your Name
 * Author URI: https://example.com
 * License: GPL v2 or later
 * License URI: https://www.gnu.org/licenses/gpl-2.0.html
 * Text Domain: my-plugin
 * Domain Path: /languages
 */

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

// Define plugin constants
define('MY_PLUGIN_VERSION', '1.0.0');
define('MY_PLUGIN_PATH', plugin_dir_path(__FILE__));
define('MY_PLUGIN_URL', plugin_dir_url(__FILE__));

// Main plugin class
class MyPlugin {
    
    public function __construct() {
        add_action('init', array($this, 'init'));
        add_action('admin_menu', array($this, 'admin_menu'));
    }
    
    public function init() {
        // Initialize plugin
        load_plugin_textdomain('my-plugin', false, dirname(plugin_basename(__FILE__)) . '/languages');
    }
    
    public function admin_menu() {
        add_menu_page(
            'My Plugin',
            'My Plugin',
            'manage_options',
            'my-plugin',
            array($this, 'admin_page'),
            'dashicons-admin-generic'
        );
    }
    
    public function admin_page() {
        include MY_PLUGIN_PATH . 'admin/admin-page.php';
    }
}

// Initialize plugin
new MyPlugin();

Activation, Deactivation, and Uninstall Hooks

// Activation hook
register_activation_hook(__FILE__, 'my_plugin_activate');

function my_plugin_activate() {
    // Create database tables
    global $wpdb;
    
    $table_name = $wpdb->prefix . 'my_plugin_data';
    
    $charset_collate = $wpdb->get_charset_collate();
    
    $sql = "CREATE TABLE $table_name (
        id mediumint(9) NOT NULL AUTO_INCREMENT,
        title varchar(255) NOT NULL,
        content text NOT NULL,
        created_at datetime DEFAULT CURRENT_TIMESTAMP,
        PRIMARY KEY (id)
    ) $charset_collate;";
    
    require_once(ABSPATH . 'wp-admin/includes/upgrade.php');
    dbDelta($sql);
    
    // Set default options
    add_option('my_plugin_version', MY_PLUGIN_VERSION);
}

// Deactivation hook
register_deactivation_hook(__FILE__, 'my_plugin_deactivate');

function my_plugin_deactivate() {
    // Clean up scheduled events
    wp_clear_scheduled_hook('my_plugin_cron');
}

// Uninstall hook (in uninstall.php)
function my_plugin_uninstall() {
    // Remove database tables
    global $wpdb;
    $wpdb->query("DROP TABLE IF EXISTS {$wpdb->prefix}my_plugin_data");
    
    // Remove options
    delete_option('my_plugin_version');
}

Security Essentials

Nonces

// Create nonce
$nonce = wp_create_nonce('my_plugin_action');

// Verify nonce
if (wp_verify_nonce($_POST['nonce'], 'my_plugin_action')) {
    // Action is secure
} else {
    wp_die('Security check failed');
}

Escaping

// Escape output
echo esc_html($user_input);
echo esc_attr($user_input);
echo esc_url($user_input);
echo wp_kses_post($user_input); // Allow safe HTML

Sanitization

// Sanitize input
$clean_input = sanitize_text_field($_POST['user_input']);
$clean_email = sanitize_email($_POST['email']);
$clean_url = esc_url_raw($_POST['url']);

Hooks, Actions, and Filters

Understanding the WordPress Event System

WordPress uses a hook system that allows plugins to "hook into" specific points in the execution flow:

Action Hooks

// Hook into WordPress actions
add_action('wp_head', 'my_plugin_head_function');
add_action('wp_footer', 'my_plugin_footer_function');
add_action('save_post', 'my_plugin_save_post_function', 10, 2);

Filter Hooks

// Modify WordPress data
add_filter('the_content', 'my_plugin_content_filter');
add_filter('the_title', 'my_plugin_title_filter');
add_filter('wp_mail_content_type', 'my_plugin_mail_filter');

Writing Custom Actions and Filters

// Define custom action
do_action('my_plugin_custom_action', $param1, $param2);

// Hook into custom action
add_action('my_plugin_custom_action', 'my_custom_function', 10, 2);

// Define custom filter
$result = apply_filters('my_plugin_custom_filter', $value, $param1);

// Hook into custom filter
add_filter('my_plugin_custom_filter', 'my_custom_filter_function', 10, 2);

Working with Custom Post Types & Taxonomies

Registering Custom Post Types

function my_plugin_register_post_types() {
    register_post_type('portfolio', array(
        'labels' => array(
            'name' => 'Portfolio',
            'singular_name' => 'Portfolio Item',
            'add_new' => 'Add New',
            'add_new_item' => 'Add New Portfolio Item',
            'edit_item' => 'Edit Portfolio Item',
            'new_item' => 'New Portfolio Item',
            'view_item' => 'View Portfolio Item',
            'search_items' => 'Search Portfolio',
            'not_found' => 'No portfolio items found',
            'not_found_in_trash' => 'No portfolio items found in trash'
        ),
        'public' => true,
        'has_archive' => true,
        'supports' => array('title', 'editor', 'thumbnail', 'excerpt'),
        'menu_icon' => 'dashicons-portfolio',
        'rewrite' => array('slug' => 'portfolio'),
        'show_in_rest' => true, // Enable Gutenberg editor
    ));
}
add_action('init', 'my_plugin_register_post_types');

Creating Custom Taxonomies

function my_plugin_register_taxonomies() {
    register_taxonomy('project_type', 'portfolio', array(
        'labels' => array(
            'name' => 'Project Types',
            'singular_name' => 'Project Type',
            'search_items' => 'Search Project Types',
            'all_items' => 'All Project Types',
            'parent_item' => 'Parent Project Type',
            'parent_item_colon' => 'Parent Project Type:',
            'edit_item' => 'Edit Project Type',
            'update_item' => 'Update Project Type',
            'add_new_item' => 'Add New Project Type',
            'new_item_name' => 'New Project Type Name',
            'menu_name' => 'Project Types'
        ),
        'hierarchical' => true,
        'show_ui' => true,
        'show_admin_column' => true,
        'query_var' => true,
        'rewrite' => array('slug' => 'project-type'),
        'show_in_rest' => true,
    ));
}
add_action('init', 'my_plugin_register_taxonomies');

Using WP_Query for Advanced Queries

// Query custom post types
$portfolio_query = new WP_Query(array(
    'post_type' => 'portfolio',
    'posts_per_page' => 6,
    'tax_query' => array(
        array(
            'taxonomy' => 'project_type',
            'field' => 'slug',
            'terms' => 'web-design',
        ),
    ),
    'meta_query' => array(
        array(
            'key' => 'featured_project',
            'value' => '1',
            'compare' => '='
        )
    )
));

if ($portfolio_query->have_posts()) {
    while ($portfolio_query->have_posts()) {
        $portfolio_query->the_post();
        // Display portfolio item
    }
    wp_reset_postdata();
}

Database and Options API

Working with wpdb Safely

global $wpdb;

// Prepare statements to prevent SQL injection
$results = $wpdb->get_results(
    $wpdb->prepare(
        "SELECT * FROM {$wpdb->prefix}my_plugin_data WHERE status = %s AND user_id = %d",
        'active',
        get_current_user_id()
    )
);

// Insert data safely
$wpdb->insert(
    $wpdb->prefix . 'my_plugin_data',
    array(
        'title' => sanitize_text_field($_POST['title']),
        'content' => sanitize_textarea_field($_POST['content']),
        'user_id' => get_current_user_id()
    ),
    array('%s', '%s', '%d')
);

// Update data safely
$wpdb->update(
    $wpdb->prefix . 'my_plugin_data',
    array('status' => 'inactive'),
    array('id' => $post_id),
    array('%s'),
    array('%d')
);

Using the Options API vs. Transients API

Options API

// Store data permanently
add_option('my_plugin_setting', 'value');
update_option('my_plugin_setting', 'new_value');
$value = get_option('my_plugin_setting', 'default_value');

// Store arrays
update_option('my_plugin_settings', array(
    'setting1' => 'value1',
    'setting2' => 'value2'
));

Transients API

// Store data temporarily (with expiration)
set_transient('my_plugin_cache', $data, HOUR_IN_SECONDS);
$cached_data = get_transient('my_plugin_cache');

// Delete transients
delete_transient('my_plugin_cache');

Building Custom Tables

function my_plugin_create_tables() {
    global $wpdb;
    
    $charset_collate = $wpdb->get_charset_collate();
    
    $sql = "CREATE TABLE {$wpdb->prefix}my_plugin_orders (
        id bigint(20) NOT NULL AUTO_INCREMENT,
        user_id bigint(20) NOT NULL,
        order_number varchar(50) NOT NULL,
        total decimal(10,2) NOT NULL,
        status varchar(20) NOT NULL DEFAULT 'pending',
        created_at datetime DEFAULT CURRENT_TIMESTAMP,
        updated_at datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
        PRIMARY KEY (id),
        KEY user_id (user_id),
        KEY order_number (order_number),
        KEY status (status)
    ) $charset_collate;";
    
    require_once(ABSPATH . 'wp-admin/includes/upgrade.php');
    dbDelta($sql);
}

REST API Development

Creating Custom REST Endpoints

function my_plugin_register_rest_routes() {
    register_rest_route('my-plugin/v1', '/data', array(
        'methods' => 'GET',
        'callback' => 'my_plugin_get_data',
        'permission_callback' => 'my_plugin_check_permissions',
    ));
    
    register_rest_route('my-plugin/v1', '/data', array(
        'methods' => 'POST',
        'callback' => 'my_plugin_create_data',
        'permission_callback' => 'my_plugin_check_permissions',
    ));
}
add_action('rest_api_init', 'my_plugin_register_rest_routes');

function my_plugin_get_data($request) {
    global $wpdb;
    
    $results = $wpdb->get_results("SELECT * FROM {$wpdb->prefix}my_plugin_data");
    
    return new WP_REST_Response($results, 200);
}

function my_plugin_create_data($request) {
    $params = $request->get_params();
    
    global $wpdb;
    
    $result = $wpdb->insert(
        $wpdb->prefix . 'my_plugin_data',
        array(
            'title' => sanitize_text_field($params['title']),
            'content' => sanitize_textarea_field($params['content'])
        )
    );
    
    if ($result) {
        return new WP_REST_Response(array('success' => true), 201);
    } else {
        return new WP_Error('insert_failed', 'Failed to insert data', array('status' => 500));
    }
}

function my_plugin_check_permissions() {
    return current_user_can('manage_options');
}

Authentication & Permissions

// Check user capabilities
function my_plugin_check_user_permissions($request) {
    $user_id = get_current_user_id();
    
    if (!$user_id) {
        return false;
    }
    
    $user = get_userdata($user_id);
    
    // Check specific capabilities
    if (!current_user_can('edit_posts')) {
        return false;
    }
    
    return true;
}

// Add custom authentication
function my_plugin_authenticate_request($request) {
    $auth_header = $request->get_header('Authorization');
    
    if (!$auth_header) {
        return false;
    }
    
    // Validate API key or token
    $api_key = str_replace('Bearer ', '', $auth_header);
    
    if ($api_key !== get_option('my_plugin_api_key')) {
        return false;
    }
    
    return true;
}

Using WordPress as a Headless CMS

// Disable frontend for headless setup
function my_plugin_headless_redirect() {
    if (!is_admin() && !wp_doing_ajax() && !wp_doing_cron()) {
        wp_redirect('https://your-frontend-app.com');
        exit;
    }
}
add_action('template_redirect', 'my_plugin_headless_redirect');

// Add CORS headers for frontend
function my_plugin_add_cors_headers() {
    header('Access-Control-Allow-Origin: https://your-frontend-app.com');
    header('Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS');
    header('Access-Control-Allow-Headers: Content-Type, Authorization');
}
add_action('rest_api_init', 'my_plugin_add_cors_headers');

Next Steps

You've now learned the fundamentals of WordPress plugin development! In Part 4: Advanced Developer Skills, you'll build a real-world plugin and learn about internationalization, testing, and debugging.


Resources: