# Wordpress Standards

> 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.

- Skill: `iwritec0de/wordpress-standards` (Agent Skill, multi-file: 2 files)
- Install (CLI): `npx skillmds@latest add iwritec0de/wordpress-standards`
- Raw SKILL.md: https://api.skillmd.com/api/skills/iwritec0de/wordpress-standards/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Docs & Writing
- License: MIT
- Author: iwritec0de (https://skillmd.com/u/iwritec0de)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/iwritec0de/wordpress-standards

---


# 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:

```php
// 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:

```php
// 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)

```php
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
// 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:

```php
/**
 * Requires PHP: 7.4
 */
```

And enforce it at runtime:

```php
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
```php
// 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:

```php
// CORRECT (Yoda):
if ( 'active' === $status ) {}
if ( true === $is_valid ) {}
if ( null !== $value ) {}

// WRONG:
if ( $status === 'active' ) {}
if ( $is_valid === true ) {}
```

### Brace Style
```php
// 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
```php
// 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
```php
// 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 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
<?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
<?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

```php
/**
 * 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")
- `@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

```php
/**
 * 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 `@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

```php
/**
 * 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

```php
/**
 * 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:

```php
/**
 * 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

```php
// 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:

```php
/* 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

```php
// 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 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)

```php
// 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`.

