wp-plugins

WordPress Plugin & Theme Development

Safety Notice

This listing is imported from skills.sh public index metadata. Review upstream SKILL.md and repository scripts before running.

Copy this and send it to your AI assistant to learn

Install skill "wp-plugins" with this command: npx skills add peixotorms/odinlayer-skills/peixotorms-odinlayer-skills-wp-plugins

WordPress Plugin & Theme Development

Consolidated reference for plugin architecture, lifecycle, settings, data storage, WP-CLI integration, static analysis, coding standards, testing, and deployment.

Resource files (detailed code examples):

  • resources/settings-api.md — Full Settings_Page class implementation

  • resources/wp-cli.md — WP-CLI custom commands and operations reference

  • resources/static-analysis-testing.md — PHPStan, PHPCS, and PHPUnit testing

  • resources/build-deploy.md — Build, deploy, and release workflows

  1. Plugin Architecture

Main Plugin File

Every plugin starts with a single PHP file containing the plugin header comment. WordPress reads this header to register the plugin in the admin UI.

<?php /**

  • Plugin Name: My Awesome Plugin
  • Plugin URI: https://example.com/my-awesome-plugin
  • Description: A short description of what this plugin does.
  • Version: 1.0.0
  • Author: Your Name
  • Author URI: https://example.com
  • License: GPL-2.0-or-later
  • Text Domain: my-awesome-plugin
  • Domain Path: /languages
  • Requires at least: 6.2
  • Requires PHP: 7.4 */

// Prevent direct access. defined( 'ABSPATH' ) || exit;

// Define constants. define( 'MAP_VERSION', '1.0.0' ); define( 'MAP_PLUGIN_DIR', plugin_dir_path( FILE ) ); define( 'MAP_PLUGIN_URL', plugin_dir_url( FILE ) ); define( 'MAP_PLUGIN_FILE', FILE );

// Autoloader (Composer PSR-4). if ( file_exists( MAP_PLUGIN_DIR . 'vendor/autoload.php' ) ) { require_once MAP_PLUGIN_DIR . 'vendor/autoload.php'; }

// Lifecycle hooks — must be registered at top level, not inside other hooks. register_activation_hook( FILE, [ 'MyAwesomePlugin\Activator', 'activate' ] ); register_deactivation_hook( FILE, [ 'MyAwesomePlugin\Deactivator', 'deactivate' ] );

// Bootstrap the plugin on plugins_loaded. add_action( 'plugins_loaded', function () { MyAwesomePlugin\Plugin::instance()->init(); } );

Bootstrap Pattern

  • The main file loads, defines constants, requires the autoloader, registers lifecycle hooks, and defers initialization to a plugins_loaded callback.

  • Avoid heavy side effects at file load time. Load on hooks.

  • Keep admin-only code behind is_admin() checks or admin-specific hooks.

Namespaces and Autoloading (PSR-4)

Configure in composer.json :

{ "autoload": { "psr-4": { "MyAwesomePlugin\": "includes/" } }, "autoload-dev": { "psr-4": { "MyAwesomePlugin\Tests\": "tests/" } } }

Run composer dump-autoload after changes.

Folder Structure

my-awesome-plugin/ my-awesome-plugin.php # Main plugin file with header uninstall.php # Cleanup on uninstall composer.json / phpstan.neon / phpcs.xml / .distignore includes/ # Core PHP classes (PSR-4 root) Plugin.php / Activator.php / Deactivator.php Admin/ Frontend/ CLI/ REST/ admin/ public/ # View partials assets/ templates/ languages/ tests/

Singleton vs Dependency Injection

Use singleton for the root plugin class only. Prefer dependency injection for everything else (testability).

namespace MyAwesomePlugin;

class Plugin { private static ?Plugin $instance = null;

public static function instance(): Plugin {
    if ( null === self::$instance ) {
        self::$instance = new self();
    }
    return self::$instance;
}

public function init(): void {
    ( new Admin\Settings_Page() )->register();
    ( new Frontend\Assets() )->register();
    if ( defined( 'WP_CLI' ) &#x26;&#x26; WP_CLI ) {
        CLI\Commands::register();
    }
}

}

Action / Filter Hook Architecture

  • Actions execute code at a specific point (do_action / add_action ).

  • Filters modify data and return it (apply_filters / add_filter ).

  • Always specify the priority (default 10) and accepted args count.

  • Provide your own hooks so other developers can extend your plugin.

// Registering hooks in your plugin. add_action( 'init', [ $this, 'register_post_types' ] ); add_filter( 'the_content', [ $this, 'append_cta' ], 20 );

// Providing extensibility. $output = apply_filters( 'map_formatted_price', $formatted, $raw_price ); do_action( 'map_after_order_processed', $order_id );

  1. Lifecycle Hooks

Lifecycle hooks must be registered at top-level scope in the main plugin file, not inside other hooks or conditional blocks.

Activation

namespace MyAwesomePlugin;

class Activator { public static function activate(): void { self::create_tables();

    if ( false === get_option( 'map_settings' ) ) {
        update_option( 'map_settings', [
            'enabled' => true, 'api_key' => '', 'max_results' => 10,
        ], false );
    }

    update_option( 'map_db_version', MAP_VERSION, false );

    // Register CPTs first, then flush so rules exist.
    ( new Plugin() )->register_post_types();
    flush_rewrite_rules();
}

private static function create_tables(): void {
    global $wpdb;
    $table = $wpdb->prefix . 'map_logs';
    $sql = "CREATE TABLE {$table} (
        id bigint(20) unsigned NOT NULL AUTO_INCREMENT,
        user_id bigint(20) unsigned NOT NULL DEFAULT 0,
        action varchar(100) NOT NULL DEFAULT '',
        data longtext NOT NULL DEFAULT '',
        created_at datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
        PRIMARY KEY (id),
        KEY user_id (user_id)
    ) {$wpdb->get_charset_collate()};";

    require_once ABSPATH . 'wp-admin/includes/upgrade.php';
    dbDelta( $sql );
}

}

Deactivation

namespace MyAwesomePlugin;

class Deactivator { public static function deactivate(): void { // Clear scheduled cron events. wp_clear_scheduled_hook( 'map_daily_cleanup' ); wp_clear_scheduled_hook( 'map_hourly_sync' );

    // Flush rewrite rules to remove custom rewrites.
    flush_rewrite_rules();
}

}

Uninstall

Create uninstall.php in the plugin root (preferred over register_uninstall_hook ):

<?php // uninstall.php — runs when plugin is deleted via admin UI. defined( 'WP_UNINSTALL_PLUGIN' ) || exit;

global $wpdb; delete_option( 'map_settings' ); delete_option( 'map_db_version' ); $wpdb->query( "DELETE FROM {$wpdb->postmeta} WHERE meta_key LIKE 'map_%'" ); $wpdb->query( "DROP TABLE IF EXISTS {$wpdb->prefix}map_logs" ); $wpdb->query( "DELETE FROM {$wpdb->options} WHERE option_name LIKE 'transient_map%' OR option_name LIKE 'transient_timeout_map%'" ); $wpdb->query( "DELETE FROM {$wpdb->usermeta} WHERE meta_key LIKE 'map_%'" );

Key rule: Never run expensive operations on every request. Activation, deactivation, and uninstall hooks exist precisely so you can perform setup and teardown only when needed.

  1. Settings API

Full Settings_Page class implementation: see resources/settings-api.md .

Options API

$settings = get_option( 'map_settings', [] ); // Read. update_option( 'map_settings', $new_values ); // Write. delete_option( 'map_settings' ); // Delete. update_option( 'map_large_cache', $data, false ); // autoload=false for infrequent options.

  1. Data Storage

When to Use What

Storage Use Case Size Guidance

Options API Small config, plugin settings < 1 MB per option

Post meta Per-post data tied to a specific post Keyed per post

User meta Per-user preferences or state Keyed per user

Custom tables Structured, queryable, or large datasets No practical limit

Transients Cached data with expiration Temporary, auto-expires

Custom Tables with dbDelta

dbDelta() rules: each field on its own line, two spaces between name and type, PRIMARY KEY with two spaces before, use KEY not INDEX .

function map_create_table(): void { global $wpdb; $sql = "CREATE TABLE {$wpdb->prefix}map_events ( id bigint(20) unsigned NOT NULL AUTO_INCREMENT, name varchar(255) NOT NULL DEFAULT '', status varchar(20) NOT NULL DEFAULT 'draft', event_date datetime NOT NULL DEFAULT '0000-00-00 00:00:00', PRIMARY KEY (id), KEY status (status) ) {$wpdb->get_charset_collate()};";

require_once ABSPATH . 'wp-admin/includes/upgrade.php';
dbDelta( $sql );

}

Schema Upgrades

Store a version and compare on plugins_loaded :

add_action( 'plugins_loaded', function () { if ( version_compare( get_option( 'map_db_version', '0' ), MAP_VERSION, '<' ) ) { map_create_table(); // dbDelta handles ALTER for existing tables. update_option( 'map_db_version', MAP_VERSION, false ); } } );

Transients

$data = get_transient( 'map_api_results' ); if ( false === $data ) { $data = map_fetch_from_api(); set_transient( 'map_api_results', $data, HOUR_IN_SECONDS ); }

Safe SQL

Always use $wpdb->prepare() for user input. Never concatenate variables into SQL.

$results = $wpdb->get_results( $wpdb->prepare( "SELECT * FROM {$wpdb->prefix}map_events WHERE status = %s AND event_date > %s", $status, $date ) );

  1. WP-CLI, Static Analysis, Testing, Build & Deploy

Detailed references in resource files:

  • WP-CLI (custom commands, operations reference): resources/wp-cli.md

  • PHPStan, PHPCS, Testing: resources/static-analysis-testing.md

  • Build & Deploy (Composer, JS/CSS, SVN, GitHub releases): resources/build-deploy.md

  1. Security Checklist
  • Sanitize on input, escape on output. Never trust $_POST /$_GET directly.

  • Use wp_unslash() before sanitizing superglobals. Read explicit keys only.

  • Pair nonce checks with current_user_can() . Nonces prevent CSRF, not authz.

  • Use $wpdb->prepare() for all SQL with user input.

  • Escape output: esc_html() , esc_attr() , esc_url() , wp_kses_post() .

  1. WordPress 6.7-6.9 Breaking Changes

WP 6.7 -- Translation Loading

load_plugin_textdomain() is auto-called for plugins with a Text Domain header. If you call it manually from init , it may load before the preferred translation source (language packs). Move manual calls to after_setup_theme or remove them entirely if the header is set.

WP 6.8 -- Bcrypt Password Hashing

WordPress 6.8 migrates from phpass MD5 to bcrypt (password_hash() ). Existing hashes upgrade transparently on next login. Impact on plugins:

  • wp_check_password() / wp_hash_password() still work -- use these, never hash passwords manually.

  • Plugins that store or compare raw $hash strings from the user_pass column will break if they assume $P$ prefix.

  • Custom authentication that bypasses wp_check_password() must handle both $P$ (legacy) and $2y$ (bcrypt) prefixes.

WP 6.9 -- Abilities API & WP_Dependencies Deprecation

  • WP_Dependencies class is deprecated. Use wp_enqueue_script() / wp_enqueue_style() -- never instantiate WP_Dependencies directly.

  • Abilities API (register_ability() ) replaces ad-hoc current_user_can() for REST route permissions (see wp-rest-api skill).

  1. Common Mistakes

Mistake Why It Fails Fix

Lifecycle hooks inside add_action('init', ...)

Activation/deactivation hooks not detected Register at top-level scope in main file

flush_rewrite_rules() without registering CPTs first Rules flushed before custom post types exist Call CPT registration, then flush

Missing sanitize_callback on register_setting()

Unsanitized data saved to database Always provide sanitize callback

autoload left as yes for large/infrequent options Every page load fetches unused data Pass false as 4th arg to update_option()

Using $wpdb->query() with string concatenation SQL injection vulnerability Use $wpdb->prepare()

Nonce check without capability check CSRF prevented but no authorization Always pair nonce + current_user_can()

innerHTML = in admin JS XSS vector Use textContent or DOM creation methods

No defined('ABSPATH') guard Direct file access possible Add defined('ABSPATH') || exit; to every PHP file

Running dbDelta() on every request Slow table introspection on every page load Run only on activation or version upgrade

Not checking WP_UNINSTALL_PLUGIN in uninstall.php File could be loaded outside uninstall context Check constant before running cleanup

PHPStan baseline grows unchecked New errors silently added to baseline Review baseline changes in PRs; never baseline new code

Missing --dry-run on wp search-replace

Irreversible changes to production database Always dry-run first, then backup, then run

Forgetting --url= in multisite WP-CLI Command hits wrong site Always include --url= for site-specific operations

Source Transparency

This detail page is rendered from real SKILL.md content. Trust labels are metadata-based hints, not a safety guarantee.

Related Skills

Related by shared tags or category signals.

Coding

elementor-development

No summary provided by upstream source.

Repository SourceNeeds Review
Coding

rust-cli

No summary provided by upstream source.

Repository SourceNeeds Review
General

elementor-hooks

No summary provided by upstream source.

Repository SourceNeeds Review
General

elementor-controls

No summary provided by upstream source.

Repository SourceNeeds Review