Wordpress standards
WordPress development plugin for Claude Code
npx -y skills add iwritec0de/wp-dev --skill wordpress-standardsAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
One thing to look at
- 1 stars1 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
What its author says it does
Copied from the file, not written here
This skill should be used when the user asks to "follow WordPress coding standards", "name functions correctly", "organize plugin files", "write DocBlocks", "set up WPCS", "use Yoda conditions", or mentions "WordPress coding standards", "WPCS", "naming conventions", "file organization", "DocBlock", "text domain", "i18n", "autoloading", "plugin structure", "theme structure". Provides WordPress coding standards and conventions including naming, file organization, coding style, and documentation standards as defined by WPCS.
The file declares its own license as MIT. That is the author’s claim about this one file, and it is not the same thing as the license GitHub reports for the repository, which is listed with the other numbers below.
SKILL.md
22.4 KB, as published. Nobody here has run it
WordPress Coding Standards & Conventions
This skill covers WordPress naming conventions, file organization, coding style, and documentation standards as defined by the WordPress Coding Standards (WPCS).
File Naming
| Type | Pattern | Example |
|---|---|---|
| Class file | class-{name}.php | class-my-plugin-admin.php |
| Interface | interface-{name}.php | interface-my-plugin-handler.php |
| Trait | trait-{name}.php | trait-my-plugin-singleton.php |
| Template | template-{name}.php | template-full-width.php |
| Template part | {section}-{name}.php | content-single.php |
| Admin page | admin-{page}.php | admin-settings.php |
| Include | descriptive lowercase | helpers.php, post-types.php |
Function & Hook Naming
All functions, hooks, and global variables must be prefixed with the plugin/theme slug:
// Functions: prefix_action_description — always include return types
function myplugin_register_post_types(): void {}
function myplugin_enqueue_admin_scripts(): void {}
function myplugin_get_option( string $key ): string {}
// Hooks: prefix/action_description
do_action( 'myplugin_after_save', $post_id );
$value = apply_filters( 'myplugin_option_value', $value, $key );
// Classes: Prefix_Descriptive_Name (or namespaced)
class MyPlugin_Admin_Settings {}
// Or with namespaces:
namespace MyPlugin\Admin;
class Settings {}
Naming Conventions
| Element | Convention | Example |
|---|---|---|
| Functions | snake_case with prefix | myplugin_get_user_data() |
| Classes | Upper_Snake_Case or namespaced | MyPlugin_Data_Handler |
| Methods | snake_case | $this->get_items() |
| Constants | UPPER_SNAKE_CASE with prefix | MYPLUGIN_VERSION |
| Variables | $snake_case | $user_name |
| Options | prefix_option_name | myplugin_settings |
| Post meta | _prefix_meta_key (leading _ = hidden) | _myplugin_custom_field |
| Transients | prefix_cache_key | myplugin_api_response |
| Nonce actions | prefix_action | myplugin_save_settings |
| Text domain | match plugin/theme slug | my-plugin |
PHP Version Requirements
Minimum PHP version: 7.4. All code must use PHP 7.4+ features and avoid deprecated patterns.
Return Type Declarations (Required)
Every function and method must declare a return type:
// Always declare return types:
function myplugin_get_option( string $key ): string {}
function myplugin_get_items(): array {}
function myplugin_is_active(): bool {}
function myplugin_register_hooks(): void {}
function myplugin_get_instance(): self {}
function myplugin_find_post( int $id ): ?WP_Post {} // Nullable
// Class methods too:
public function init(): void {}
public function get_settings(): array {}
private function sanitize_input( string $input ): string {}
protected function build_query( array $args ): WP_Query {}
Typed Properties (Required for Classes)
class MyPlugin_Settings {
private string $option_group = 'myplugin_settings';
private array $defaults = [];
private bool $initialized = false;
private ?string $api_key = null;
protected int $cache_ttl = 3600;
}
PHP 7.4+ Features to Use
| Feature | Example | Notes |
|---|---|---|
| Typed properties | private string $name; | All class properties should be typed |
| Return types | function get(): string {} | Required on all functions/methods |
| Null coalescing assignment | $value ??= 'default'; | Cleaner than if ( null === $value ) |
| Arrow functions | fn( $item ) => $item->ID | Use for simple callbacks |
| Short array syntax | [ 'key' => 'value' ] | Preferred over array() |
| Spread operator | array_merge( ...$arrays ) | Useful for merging arrays |
| Type declarations | function foo( int $id, string $name ): void | All params should be typed where possible |
PHP 8.x Deprecations & Compatibility
When targeting PHP 8.0+, be aware of these changes:
// PHP 8.0: Union types (use when min PHP is 8.0+)
function myplugin_process( string|array $input ): string|WP_Error {}
// PHP 8.0: match expression (preferred over switch for value returns)
$label = match ( $status ) {
'publish' => __( 'Published', 'myplugin' ),
'draft' => __( 'Draft', 'myplugin' ),
default => __( 'Unknown', 'myplugin' ),
};
// PHP 8.0: Named arguments (use sparingly — breaks if param names change)
wp_insert_post( title: 'Hello', status: 'publish' );
// PHP 8.0: Nullsafe operator
$name = $order?->get_billing_address()?->get_city();
Deprecated patterns to avoid:
| Deprecated | Version | Replacement |
|---|---|---|
${var} in strings | 8.2 | {$var} (always use curly braces around $) |
utf8_encode() / utf8_decode() | 8.2 | mb_convert_encoding() |
strftime() | 8.1 | wp_date() or date_i18n() (WordPress), IntlDateFormatter (PHP) |
Return by reference from void function &foo(): void | 8.1 | Remove the & — returning by reference from a void function is contradictory |
| Dynamic properties on classes | 8.2 | Declare all properties explicitly or use #[AllowDynamicProperties] |
Implicit nullable types function( Type $x = null ) | 8.4 | function( ?Type $x = null ) — use explicit nullable |
create_function() | 7.2 (removed 8.0) | Anonymous functions: function() {} |
each() | 7.2 (removed 8.0) | foreach loop |
mysql_* functions | 5.5 (removed 7.0) | $wpdb methods |
ereg*() functions | 5.3 (removed 7.0) | preg_match() |
Plugin Header PHP Version
Always declare the minimum PHP version in the plugin header:
/**
* Requires PHP: 7.4
*/
And enforce it at runtime:
if ( version_compare( PHP_VERSION, '7.4', '<' ) ) {
add_action( 'admin_notices', function (): void {
printf(
'<div class="notice notice-error"><p>%s</p></div>',
esc_html__( 'My Plugin requires PHP 7.4 or higher.', 'myplugin' )
);
} );
return;
}
PHP Coding Style
Whitespace
// Spaces inside parentheses:
if ( $condition ) {
my_function( $arg1, $arg2 );
}
// Spaces around operators:
$result = $a + $b;
$check = ( $a === $b );
// No space before array access:
$value = $array['key'];
$item = $array[ $index ]; // Space OK with variables
// Tabs for indentation (not spaces)
Yoda Conditions
Place the constant/literal on the left side of comparisons:
// CORRECT (Yoda):
if ( 'active' === $status ) {}
if ( true === $is_valid ) {}
if ( null !== $value ) {}
// WRONG:
if ( $status === 'active' ) {}
if ( $is_valid === true ) {}
Brace Style
// Functions/classes — opening brace on same line (WP style):
function myplugin_example( string $param ): void {
if ( $param ) {
// ...
} elseif ( $other ) {
// ...
} else {
// ...
}
}
// Single-line conditions still use braces:
if ( $condition ) {
return true;
}
Array Syntax
// Short array syntax is allowed in modern WP (5.4+):
$args = array(
'post_type' => 'post',
'posts_per_page' => 10,
'orderby' => 'date',
);
// Also acceptable:
$args = [
'post_type' => 'post',
'posts_per_page' => 10,
];
String Interpolation
// Use single quotes when no interpolation is needed:
$name = 'WordPress';
// Use sprintf for complex interpolation:
$message = sprintf(
/* translators: %s: user display name */
esc_html__( 'Hello, %s!', 'myplugin' ),
esc_html( $user->display_name )
);
File Organization
Plugin Structure (Small to Medium)
my-plugin/
├── my-plugin.php # Main plugin file (bootstrap)
├── uninstall.php # Cleanup on uninstall
├── readme.txt # WordPress.org readme
├── composer.json # Dependencies + PSR-4 autoloading
├── package.json # JS build tools (optional)
├── phpcs.xml.dist # WPCS config
├── includes/ # Core PHP classes
│ ├── class-plugin.php # Main singleton
│ ├── class-admin.php # Admin UI
│ └── class-rest-api.php # REST endpoints
├── admin/ # Admin-specific assets & views
│ ├── css/
│ ├── js/
│ └── views/
├── public/ # Front-end assets & views
│ ├── css/
│ ├── js/
│ └── views/
├── assets/ # Shared assets
│ ├── images/
│ └── fonts/
├── languages/
│ └── my-plugin.pot
├── templates/ # Overridable templates
└── tests/
├── bootstrap.php
└── test-my-plugin.php
Plugin Structure (Large / Enterprise)
For larger plugins with many features, use a domain-organized includes/ directory:
my-plugin/
├── my-plugin.php # Bootstrap: constants, autoload, activation hooks
├── uninstall.php
├── readme.txt
├── composer.json # PSR-4: "MyPlugin\\": "includes/"
├── package.json
├── phpcs.xml.dist
├── includes/ # All PHP classes (PSR-4 root)
│ ├── Plugin.php # Main singleton — registers all modules
│ ├── Admin/ # Admin UI, settings pages, admin AJAX
│ │ ├── Settings.php
│ │ ├── Menu.php
│ │ └── views/ # Admin page templates
│ ├── PostTypes/ # CPT & taxonomy registration
│ │ ├── Event.php
│ │ └── EventCategory.php
│ ├── REST/ # REST API controllers
│ │ ├── EventController.php
│ │ └── SettingsController.php
│ ├── Database/ # Custom tables, migrations, queries
│ │ ├── Migrator.php
│ │ └── EventTable.php
│ ├── Services/ # Business logic (non-WordPress)
│ │ ├── ImportService.php
│ │ └── NotificationService.php
│ ├── Integrations/ # Third-party integrations
│ │ ├── WooCommerce.php
│ │ └── ACF.php
│ ├── Blocks/ # Gutenberg blocks (PHP side)
│ │ └── EventBlock.php
│ ├── CLI/ # WP-CLI commands
│ │ └── Commands.php
│ └── Utils/ # Helpers, formatters, shared utilities
│ ├── Formatter.php
│ └── Logger.php
├── assets/ # Static assets (images, fonts)
├── admin/ # Admin CSS/JS (or use build/ for compiled)
│ ├── css/
│ └── js/
├── public/ # Frontend CSS/JS
│ ├── css/
│ └── js/
├── src/ # JS/block source (compiled to build/)
│ └── blocks/
├── build/ # Compiled JS/CSS (gitignored)
├── languages/
│ └── my-plugin.pot
├── templates/ # Overridable theme templates
└── tests/
├── bootstrap.php
├── Unit/
└── Integration/
Key principles for large plugin layout:
- All PHP classes live under
includes/with PSR-4 autoloading - Organize by domain (PostTypes, REST, Admin) not by file type
Services/holds business logic decoupled from WordPress APIsDatabase/encapsulates custom table creation and query buildersUtils/is for genuinely shared helpers — avoid dumping everything here- Each class has a single responsibility; the main
Plugin.phpwires them together
Theme Structure
my-theme/
├── style.css # Theme metadata + base styles
├── functions.php # Theme setup
├── index.php # Fallback template
├── header.php
├── footer.php
├── sidebar.php
├── single.php
├── page.php
├── archive.php
├── search.php
├── 404.php
├── front-page.php # Static front page
├── home.php # Blog posts page
├── inc/ # Includes
│ ├── customizer.php
│ ├── template-tags.php
│ └── template-functions.php
├── template-parts/ # Reusable partials
│ ├── content.php
│ ├── content-page.php
│ └── content-none.php
├── assets/
│ ├── css/
│ ├── js/
│ └── images/
└── languages/
PHPDoc Standards
Every PHP file, function, class, method, property, constant, hook, and filter must have a properly formatted PHPDoc block following WordPress Inline Documentation Standards. These blocks are the primary source for auto-generated developer documentation.
File Header
Every PHP file must start with a file-level DocBlock immediately after <?php:
<?php
/**
* My Plugin Admin Settings
*
* Handles the admin settings page for My Plugin.
*
* @package MyPlugin
* @subpackage Admin
* @since 1.0.0
*/
defined( 'ABSPATH' ) || exit;
The main plugin file additionally requires:
<?php
/**
* Plugin Name: My Plugin
* Plugin URI: https://example.com/my-plugin
* Description: Short description of the plugin.
* Version: 1.0.0
* Author: Author Name
* Author URI: https://example.com
* License: GPL-2.0-or-later
* License URI: https://www.gnu.org/licenses/gpl-2.0.html
* Text Domain: my-plugin
* Domain Path: /languages
* Requires at least: 6.0
* Requires PHP: 7.4
*
* @package MyPlugin
*/
Function DocBlock
/**
* Short description (one line, imperative mood: "Retrieves", "Registers", "Processes").
*
* Long description if needed. Explain what the function does, any side effects,
* and important behavioral notes. Use complete sentences.
*
* @since 1.0.0
* @since 1.2.0 Added $default parameter.
*
* @see some_related_function() For related context.
* @link https://developer.wordpress.org/reference/... External reference.
*
* @global wpdb $wpdb WordPress database abstraction object.
*
* @param string $key The settings key to retrieve.
* @param mixed $default Optional. Default value. Default empty string.
* @return mixed The setting value, or $default if not found.
*/
function myplugin_get_setting( string $key, mixed $default = '' ): mixed {}
Rules:
- Short description is required — one line, imperative mood ("Retrieves X" not "This function retrieves X")
@sinceis required — marks the version the function was introduced; add additional@sincelines for significant changes@paramis required for every parameter — format:@param type $name Description.@returnis required unless the function returns void — format:@return type Description.- Align
@paramnames and descriptions vertically for readability - Use
Optional.prefix and document the default value:Optional. Default value. Default false.
Class DocBlock
/**
* Handles admin settings registration and rendering.
*
* This class manages the settings page under wp-admin, including
* field registration, sanitization callbacks, and settings sections.
*
* @since 1.0.0
* @package MyPlugin
*/
class MyPlugin_Admin_Settings {
/**
* The option group name.
*
* @since 1.0.0
* @var string
*/
private $option_group = 'myplugin_settings';
/**
* Registers the settings page and fields.
*
* Hooks into admin_menu and admin_init to register the page
* and all associated settings fields.
*
* @since 1.0.0
*
* @return void
*/
public function register(): void {}
}
Rules:
- Every class needs a DocBlock with
@sinceand@package - Every property needs
@since,@var, and@access(if not implied by visibility keyword) - Every method follows the same rules as functions
- Use
@inheritDoconly when the parent's DocBlock is sufficient — do not use it as a shortcut to skip documentation
Hook & Filter DocBlocks (Critical for Doc Generation)
Every do_action() and apply_filters() call must have a DocBlock immediately above it. These blocks are parsed by documentation generators (WP Parser, phpDocumentor, and the wp-hooks scanner in this plugin) to produce the hooks reference.
Action DocBlock
/**
* Fires after a setting has been saved to the database.
*
* This action allows plugins to perform additional processing
* whenever a plugin setting is updated, such as cache invalidation
* or syncing with external services.
*
* @since 1.0.0
* @since 1.3.0 Added $old_value parameter.
*
* @param string $key The setting key that was saved.
* @param mixed $value The new value that was saved.
* @param mixed $old_value The previous value before the update.
*/
do_action( 'myplugin_setting_saved', $key, $value, $old_value );
Filter DocBlock
/**
* Filters the retrieved setting value before it is returned.
*
* Allows modification of any setting value at retrieval time.
* Useful for applying dynamic defaults, environment overrides,
* or transformations based on context.
*
* @since 1.0.0
*
* @param mixed $value The current setting value (empty string if not set).
* @param string $key The setting key being retrieved.
* @return mixed The filtered setting value. Expected to match the original type.
*/
$value = apply_filters( 'myplugin_get_setting', $value, $key );
Dynamic Hook Names
When hook names contain variables, document the pattern and possible values:
/**
* Filters the output for a specific content type before rendering.
*
* The dynamic portion of the hook name, `$content_type`, refers to
* the content type slug. Possible values include 'post', 'page',
* 'product', or any registered custom post type.
*
* @since 1.0.0
*
* @param string $output The HTML output to render.
* @param int $post_id The post ID being rendered.
* @param array $args Additional rendering arguments.
* @return string The filtered HTML output.
*/
$output = apply_filters( "myplugin_{$content_type}_output", $output, $post_id, $args );
Hook DocBlock Rules
| Rule | Details |
|---|---|
| Placement | DocBlock must be directly above the do_action() / apply_filters() call — no blank lines or code between them |
| Short description | Actions: start with "Fires..." — Filters: start with "Filters..." |
| Long description | Explain when/why this hook fires, what it enables, and any caveats |
| @since | Required — version the hook was introduced |
| @param | Required for every parameter passed to the hook, in order |
| @return (filters only) | Required — describe expected return type and value |
| Dynamic hooks | Must document the dynamic portion, its possible values, and use the full pattern in the short description |
| No stacking | One DocBlock per hook call — do not combine multiple hooks into one block |
Inline Comments
// Single-line comments use //, a space, and a capital letter.
// They should form complete sentences with periods.
/*
* Multi-line inline comments use this format.
* Each line starts with a space and an asterisk.
*/
Common @tags Reference
| Tag | When to use |
|---|---|
@since | Every function, method, class, hook, property, constant |
@param | Every function/method parameter and every hook parameter |
@return | Every function/method that returns a value, and every filter |
@var | Every class property |
@throws | If the function throws an exception |
@see | Cross-reference to related functions, classes, or hooks |
@link | URL to external documentation |
@global | When accessing global variables |
@access | Only when visibility differs from the keyword (rare) |
@deprecated | With version and @see pointing to the replacement |
@todo | Legitimate items to address (never commit to production) |
@ignore | Exclude from generated docs (use sparingly) |
translators Comment
Always add a translators comment before strings with placeholders:
/* translators: %s: user display name */
$message = sprintf( esc_html__( 'Hello, %s!', 'my-plugin' ), esc_html( $user->display_name ) );
/* translators: 1: date, 2: time */
$label = sprintf( __( 'Published on %1$s at %2$s', 'my-plugin' ), $date, $time );
Text Domain & i18n
// Translatable strings:
__( 'Settings', 'my-plugin' ) // Return translated string
_e( 'Save Changes', 'my-plugin' ) // Echo translated string
_n( '%s item', '%s items', $count, 'my-plugin' ) // Singular/plural
_x( 'Post', 'noun', 'my-plugin' ) // Context-disambiguated
// Escaped variants (use these for output):
esc_html__( 'Title', 'my-plugin' )
esc_attr__( 'Label', 'my-plugin' )
esc_html_e( 'Heading', 'my-plugin' )
i18n Rules
- Text domain must be a string literal matching the plugin/theme slug — never a variable
- Never concatenate translatable strings — use
sprintf()with numbered placeholders (%1$s,%2$s) - Always add
/* translators: */comments before strings with placeholders - Use
number_format_i18n()for numbers andwp_date()for dates — never rawdate()ornumber_format() - Load text domain with
load_plugin_textdomain()for plugins,load_theme_textdomain()for themes - Use
wp_set_script_translations()for JavaScript translations (WP 5.0+) - Generate POT files with
wp i18n make-potand JSON withwp i18n make-json
For comprehensive i18n patterns, see the wordpress-i18n skill.
Autoloading (Modern Plugins)
// PSR-4 autoloading via Composer:
// composer.json:
{
"autoload": {
"psr-4": {
"MyPlugin\\": "src/"
}
}
}
// In main plugin file:
require_once __DIR__ . '/vendor/autoload.php';
For detailed naming convention tables, see references/naming-conventions.md.