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.