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.
124 lines
4.6 KiB
Markdown
124 lines
4.6 KiB
Markdown
---
|
|
name: wordpress-plugin-conventions
|
|
description: 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
|
|
<?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 to `manage_options` for 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`/`$_GET` value 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()`:
|
|
```php
|
|
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.
|