| name | wordpress-standards |
| description | 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. |
| license | MIT |
| metadata | {"author":"Chris Kelley (hello@iwritecode.io)","version":"1.0.0"} |
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:
function myplugin_register_post_types(): void {}
function myplugin_enqueue_admin_scripts(): void {}
function myplugin_get_option( string $key ): string {}
do_action( 'myplugin_after_save', $post_id );
$value = apply_filters( 'myplugin_option_value', $value, $key );
class MyPlugin_Admin_Settings {}
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:
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 {}
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:
function myplugin_process( string|array $input ): string|WP_Error {}
$label = match ( $status ) {
'publish' => __( 'Published', 'myplugin' ),
'draft' => __( 'Draft', 'myplugin' ),
default => __( 'Unknown', 'myplugin' ),
};
wp_insert_post( title: 'Hello', status: 'publish' );
$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:
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
if ( $condition ) {
my_function( $arg1, $arg2 );
}
$result = $a + $b;
$check = ( $a === $b );
$value = $array['key'];
$item = $array[ $index ];
Yoda Conditions
Place the constant/literal on the left side of comparisons:
if ( 'active' === $status ) {}
if ( true === $is_valid ) {}
if ( null !== $value ) {}
if ( $status === 'active' ) {}
if ( $is_valid === true ) {}
Brace Style
function myplugin_example( string $param ): void {
if ( $param ) {
} elseif ( $other ) {
} else {
}
}
if ( $condition ) {
return true;
}
Array Syntax
$args = array(
'post_type' => 'post',
'posts_per_page' => 10,
'orderby' => 'date',
);
$args = [
'post_type' => 'post',
'posts_per_page' => 10,
];
String Interpolation
$name = 'WordPress';
$message = sprintf(
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 APIs
Database/ encapsulates custom table creation and query builders
Utils/ is for genuinely shared helpers — avoid dumping everything here
- Each class has a single responsibility; the main
Plugin.php wires 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
defined( 'ABSPATH' ) || exit;
The main plugin file additionally requires:
<?php
Function DocBlock
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")
@since is required — marks the version the function was introduced; add additional @since lines for significant changes
@param is required for every parameter — format: @param type $name Description.
@return is required unless the function returns void — format: @return type Description.
- Align
@param names and descriptions vertically for readability
- Use
Optional. prefix and document the default value: Optional. Default value. Default false.
Class DocBlock
class MyPlugin_Admin_Settings {
private $option_group = 'myplugin_settings';
public function register(): void {}
}
Rules:
- Every class needs a DocBlock with
@since and @package
- Every property needs
@since, @var, and @access (if not implied by visibility keyword)
- Every method follows the same rules as functions
- Use
@inheritDoc only 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
do_action( 'myplugin_setting_saved', $key, $value, $old_value );
Filter DocBlock
$value = apply_filters( 'myplugin_get_setting', $value, $key );
Dynamic Hook Names
When hook names contain variables, document the pattern and possible values:
$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
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:
$message = sprintf( esc_html__( 'Hello, %s!', 'my-plugin' ), esc_html( $user->display_name ) );
$label = sprintf( __( 'Published on %1$s at %2$s', 'my-plugin' ), $date, $time );
Text Domain & i18n
__( 'Settings', 'my-plugin' )
_e( 'Save Changes', 'my-plugin' )
_n( '%s item', '%s items', $count, 'my-plugin' )
_x( 'Post', 'noun', 'my-plugin' )
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 and wp_date() for dates — never raw date() or number_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-pot and JSON with wp i18n make-json
For comprehensive i18n patterns, see the wordpress-i18n skill.
Autoloading (Modern Plugins)
{
"autoload": {
"psr-4": {
"MyPlugin\\": "src/"
}
}
}
require_once __DIR__ . '/vendor/autoload.php';
For detailed naming convention tables, see references/naming-conventions.md.