Loading...
Loading...
Use when building WordPress plugins or themes. Covers plugin architecture, plugin header and text domain, register_activation_hook, register_deactivation_hook, uninstall.php, settings API (add_options_page, register_setting), $wpdb and dbDelta for custom tables, schema upgrades, transients, data storage patterns, WP_CLI custom commands, PHPStan configuration, phpcs (WordPress coding standards linting), PHPUnit testing, wp scaffold plugin, PSR-4 autoloading, and build/deploy workflows.
npx skill4agent add peixotorms/odinlayer-skills wp-pluginsresources/settings-api.mdresources/wp-cli.mdresources/static-analysis-testing.mdresources/build-deploy.md<?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();
} );plugins_loadedis_admin()composer.json{
"autoload": {
"psr-4": {
"MyAwesomePlugin\\": "includes/"
}
},
"autoload-dev": {
"psr-4": {
"MyAwesomePlugin\\Tests\\": "tests/"
}
}
}composer dump-autoloadmy-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/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' ) && WP_CLI ) {
CLI\Commands::register();
}
}
}do_actionadd_actionapply_filtersadd_filter// 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 );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 );
}
}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.phpregister_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\_%'" );resources/settings-api.md$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.| 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 |
dbDelta()PRIMARY KEYKEYINDEXfunction 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 );
}plugins_loadedadd_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 );
}
} );$data = get_transient( 'map_api_results' );
if ( false === $data ) {
$data = map_fetch_from_api();
set_transient( 'map_api_results', $data, HOUR_IN_SECONDS );
}$wpdb->prepare()$results = $wpdb->get_results( $wpdb->prepare(
"SELECT * FROM {$wpdb->prefix}map_events WHERE status = %s AND event_date > %s",
$status, $date
) );resources/wp-cli.mdresources/static-analysis-testing.mdresources/build-deploy.md$_POST$_GETwp_unslash()current_user_can()$wpdb->prepare()esc_html()esc_attr()esc_url()wp_kses_post()load_plugin_textdomain()Text Domaininitafter_setup_themepassword_hash()wp_check_password()wp_hash_password()$hashuser_pass$P$wp_check_password()$P$$2y$WP_Dependencieswp_enqueue_script()wp_enqueue_style()WP_Dependenciesregister_ability()current_user_can()| Mistake | Why It Fails | Fix |
|---|---|---|
Lifecycle hooks inside | Activation/deactivation hooks not detected | Register at top-level scope in main file |
| Rules flushed before custom post types exist | Call CPT registration, then flush |
Missing | Unsanitized data saved to database | Always provide sanitize callback |
| Every page load fetches unused data | Pass |
Using | SQL injection vulnerability | Use |
| Nonce check without capability check | CSRF prevented but no authorization | Always pair nonce + |
| XSS vector | Use |
No | Direct file access possible | Add |
Running | Slow table introspection on every page load | Run only on activation or version upgrade |
Not checking | 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 | Irreversible changes to production database | Always dry-run first, then backup, then run |
Forgetting | Command hits wrong site | Always include |