WordPress plugin rebrand/conventions/remote-CLI patterns, Gitea release workflow, bastille jail provisioning, remote shell quoting safety, server fleet map, delegate brief writing, and verification discipline -- all derived from real incidents this session, plus two skills adapted (MIT license, attributed) from obra/superpowers and andrej-karpathy-skills.
4.6 KiB
name, description
| name | description |
|---|---|
| wordpress-plugin-conventions | Use when writing or modifying a WordPress plugin's PHP code. Baseline structural/security conventions to follow without being told each time. |
WordPress Plugin Conventions
Baseline conventions for first-party WordPress plugin code. Follow these without needing to be told in every brief.
File structure
plugin-slug/
plugin-slug.php — main file: header, constants, bootstrap class
includes/
class-<prefix>-*.php — one class per concern, class-based not procedural
admin/ — admin-only UI (settings pages, dashboards)
assets/{css,js}/
languages/ — .pot/.po/.mo if the plugin is translatable
Main file skeleton
<?php
/**
* Plugin Name: ...
* Plugin URI: ...
* Description: ...
* Version: 1.0.0
* Author: ...
* Author URI: ...
* License: GPL v2 or later
* License URI: https://www.gnu.org/licenses/gpl-2.0.html
* Text Domain: plugin-slug
* Requires PHP: 7.4
*/
if (!defined('ABSPATH')) {
exit;
}
define('PREFIX_VERSION', '1.0.0');
define('PREFIX_PATH', plugin_dir_path(__FILE__));
define('PREFIX_URL', plugin_dir_url(__FILE__));
require_once PREFIX_PATH . 'includes/class-prefix-thing.php';
class Prefix_Main {
private static $instance = null;
public static function instance() {
if (null === self::$instance) { self::$instance = new self(); }
return self::$instance;
}
private function __construct() { /* hook registration only */ }
public static function activate() { /* create tables/options/dirs */ }
public static function deactivate() { /* reverse activate() side effects */ }
}
register_activation_hook(__FILE__, ['Prefix_Main', 'activate']);
register_deactivation_hook(__FILE__, ['Prefix_Main', 'deactivate']);
add_action('plugins_loaded', ['Prefix_Main', 'instance']);
Every included file starts with if (!defined('ABSPATH')) exit; — never
define('ABSPATH', ...) && or any variant, exactly the guard-and-exit
form, so the file can never be requested directly over HTTP.
Every file must pass php -l before you consider a task done
Not optional, not "probably fine" — actually run it, on every changed file, every time. This is the cheapest possible check and catches a meaningful fraction of real mistakes (typos, mismatched braces from a find/replace, etc.) before they ever reach a live site.
Security baseline
- Nonces on every form/AJAX action:
wp_nonce_field()/check_admin_referer()/check_ajax_referer(). - Capability checks before any privileged action:
current_user_can('manage_options')(or the narrowest capability that actually applies — don't default tomanage_optionsfor things a lower-privileged role should legitimately be able to do). - Escape on output, every time, using the context-correct function:
esc_html(),esc_attr(),esc_url(),esc_js()— never raw-echo anything that traces back to user input or the database without one of these. - Sanitize on input:
sanitize_text_field(),absint(),sanitize_email(), etc. — appropriate to the expected shape of the data, applied at the point the$_POST/$_GETvalue is first read. $wpdb->prepare()for every query with a variable in it — no string- interpolated SQL, ever, no exceptions.
Database tables
If a plugin needs its own tables (not just options), use dbDelta():
require_once ABSPATH . 'wp-admin/includes/upgrade.php';
$charset = $wpdb->get_charset_collate();
$sql = "CREATE TABLE {$wpdb->prefix}prefix_thing (...) {$charset};";
dbDelta($sql);
Track a DB_VERSION constant + a stored option so future plugin updates
can detect and re-run dbDelta() for schema changes.
PHP 7.4 compatibility
Unless a specific brief says otherwise, target PHP 7.4+ (a large fraction
of real WordPress hosting is still on 7.4). Avoid PHP 8-only syntax
(named arguments, enums, readonly properties, match). If a codebase
needs str_contains/str_starts_with/str_ends_with (PHP 8.0+) on an
environment that might run 7.4, guard with function_exists()
polyfills rather than assuming they exist.
Don't build what WordPress core already gives you
Before writing custom code for: cron scheduling (wp_schedule_event),
REST endpoints (register_rest_route), settings UI
(register_setting/add_settings_field/woocommerce_admin_fields if
WooCommerce context), object caching (wp_cache_* functions), HTTP
requests (wp_remote_get/wp_remote_post, not raw curl) — check
whether a core API already does it. Reinventing these is both wasted
effort and a common source of subtle bugs core already solved correctly.