agentleFS
iwritec0deiwritec0de/wp-dev/skills/wordpress-standards/SKILL.md

wordpress-standards

Skill1 starsChanged 7 months agoMITView on GitHub

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

Show its rules and text

We split this skill into the rules it gives an agent

19 rules. About 0 are common, 9 are distinctive, and 10 are very specific to this project.

  • ≈0

    Common · estimate

    Many files like this have a rule of this kind.

  • ≈9

    Distinctive · estimate

    This author's own choices. Worth a look.

  • 10

    Very specific

    Tied to their project's names, files or tools. Probably skip.

  • Common
  • Distinctive
  • Very specific
  1. Distinctive:

    All functions, hooks, and global variables must be prefixed with the plugin/theme slug:

    line 35
  2. Distinctive:

    Minimum PHP version: 7.4: All code must use PHP 7.4+ features and avoid deprecated patterns.

    line 71
  3. Distinctive:

    Every function and method must declare a return type:

    line 75
  4. Distinctive:

    When targeting PHP 8.0+, be aware of these changes:

    line 119
  5. Distinctive:

    Always declare the minimum PHP version in the plugin header:

    line 156
  6. Distinctive:

    Place the constant/literal on the left side of comparisons:

    line 199
  7. Very specific:

    All PHP classes live under includes/ with PSR-4 autoloading

    line 355
  8. Distinctive:

    Every PHP file, function, class, method, property, constant, hook, and filter must have a properly formatted PHPDoc block following …

    line 395
  9. Very specific:

    Every PHP file must start with a file-level DocBlock immediately after <?php:

    line 399
  10. Very specific:

    Use Optional. prefix and document the default value: Optional.

    line 468
  11. Very specific:

    Every class needs a DocBlock with @since and @package

    line 507
  12. Very specific:

    Every property needs @since, @var, and @access (if not implied by visibility keyword)

    line 508
  13. Distinctive:

    Every method follows the same rules as functions

    line 509
  14. Very specific:

    Use @inheritDoc only when the parent's DocBlock is sufficient — do not use it as a shortcut to skip documentation

    line 510
  15. Distinctive:

    When hook names contain variables, document the pattern and possible values:

    line 557
  16. Very specific:

    Always add a translators comment before strings with placeholders:

    line 621
  17. Very specific:

    Always add /* translators: */ comments before strings with placeholders

    line 650
  18. Very specific:

    Use number_format_i18n() for numbers and wp_date() for dates — never raw date() or number_format()

    line 651
  19. Very specific:

    Use wp_set_script_translations() for JavaScript translations (WP 5.0+)

    line 653

≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.

Keep what fits and drop the rest. Your version, ready for your agents.
Files
SKILL.md

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

TypePatternExample
Class fileclass-{name}.phpclass-my-plugin-admin.php
Interfaceinterface-{name}.phpinterface-my-plugin-handler.php
Traittrait-{name}.phptrait-my-plugin-singleton.php
Templatetemplate-{name}.phptemplate-full-width.php
Template part{section}-{name}.phpcontent-single.php
Admin pageadmin-{page}.phpadmin-settings.php
Includedescriptive lowercasehelpers.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

ElementConventionExample
Functionssnake_case with prefixmyplugin_get_user_data()
ClassesUpper_Snake_Case or namespacedMyPlugin_Data_Handler
Methodssnake_case$this->get_items()
ConstantsUPPER_SNAKE_CASE with prefixMYPLUGIN_VERSION
Variables$snake_case$user_name
Optionsprefix_option_namemyplugin_settings
Post meta_prefix_meta_key (leading _ = hidden)_myplugin_custom_field
Transientsprefix_cache_keymyplugin_api_response
Nonce actionsprefix_actionmyplugin_save_settings
Text domainmatch plugin/theme slugmy-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

FeatureExampleNotes
Typed propertiesprivate string $name;All class properties should be typed
Return typesfunction get(): string {}Required on all functions/methods
Null coalescing assignment$value ??= 'default';Cleaner than if ( null === $value )
Arrow functionsfn( $item ) => $item->IDUse for simple callbacks
Short array syntax[ 'key' => 'value' ]Preferred over array()
Spread operatorarray_merge( ...$arrays )Useful for merging arrays
Type declarationsfunction foo( int $id, string $name ): voidAll 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:

DeprecatedVersionReplacement
${var} in strings8.2{$var} (always use curly braces around $)
utf8_encode() / utf8_decode()8.2mb_convert_encoding()
strftime()8.1wp_date() or date_i18n() (WordPress), IntlDateFormatter (PHP)
Return by reference from void function &foo(): void8.1Remove the & — returning by reference from a void function is contradictory
Dynamic properties on classes8.2Declare all properties explicitly or use #[AllowDynamicProperties]
Implicit nullable types function( Type $x = null )8.4function( ?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_* functions5.5 (removed 7.0)$wpdb methods
ereg*() functions5.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 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
/**
 * 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")
  • @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

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

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

RuleDetails
PlacementDocBlock must be directly above the do_action() / apply_filters() call — no blank lines or code between them
Short descriptionActions: start with "Fires..." — Filters: start with "Filters..."
Long descriptionExplain when/why this hook fires, what it enables, and any caveats
@sinceRequired — version the hook was introduced
@paramRequired for every parameter passed to the hook, in order
@return (filters only)Required — describe expected return type and value
Dynamic hooksMust document the dynamic portion, its possible values, and use the full pattern in the short description
No stackingOne 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

TagWhen to use
@sinceEvery function, method, class, hook, property, constant
@paramEvery function/method parameter and every hook parameter
@returnEvery function/method that returns a value, and every filter
@varEvery class property
@throwsIf the function throws an exception
@seeCross-reference to related functions, classes, or hooks
@linkURL to external documentation
@globalWhen accessing global variables
@accessOnly when visibility differs from the keyword (rare)
@deprecatedWith version and @see pointing to the replacement
@todoLegitimate items to address (never commit to production)
@ignoreExclude 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 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)

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

Download now (ZIP)

Does it work?

Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.

No reviews yet

People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.

  • It worked
  • Partly
  • It didn't
What did you use it for? e.g. Reviewing pull requests in a Rails app

Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.

Your trail

Files you open will collect here, so you can find them again.

Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.

Now do this for your own.

Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.