xonack

wp-coding-standards

0
0
# Install this skill:
npx skills add xonack/wp-coding-standards-claude-skill --skill "wp-coding-standards"

Install specific skill from multi-skill repository

# Description

>

# SKILL.md


name: wp-coding-standards
description: >
Enforces WordPress Coding Standards (WPCS) across PHP code. Covers WordPress-Core,
WordPress-Extra, and WordPress-VIP rulesets. Provides definitive reference for data
sanitization, output escaping, nonce verification, PHPDoc, hook patterns, database
query safety, and auto-enforcement tooling setup.
tools:
- Bash
- Read
- Edit
- Write
- Grep
- Glob


WordPress Coding Standards Enforcement

This skill is the definitive reference for writing PHP code that passes WordPress
Coding Standards (WPCS) sniffs. Every pattern includes WRONG vs RIGHT examples.


1. WordPress-Core Standard

Yoda Conditions

Place the constant or literal on the LEFT side of comparisons. This prevents
accidental assignment (= instead of ===) from silently succeeding.

// WRONG β€” variable on left risks accidental assignment
if ( $type === 'post' ) { ... }
if ( $count == 0 ) { ... }

// RIGHT β€” literal on left (Yoda style)
if ( 'post' === $type ) { ... }
if ( 0 === $count ) { ... }

Array Syntax

Always use array(). Short array syntax [] is not permitted by WordPress-Core.

// WRONG
$items = [];
$config = ['key' => 'value'];

// RIGHT
$items = array();
$config = array( 'key' => 'value' );

Spaces Inside Parentheses

Add a single space after opening and before closing parentheses in control
structures, function declarations, and function calls.

// WRONG
if ($foo) {
    my_function($bar, $baz);
}

// RIGHT
if ( $foo ) {
    my_function( $bar, $baz );
}

Naming Conventions

Functions, variables, and action/filter names use snake_case. Classes use
Upper_Snake_Case. Constants use UPPER_SNAKE_CASE. File names use lowercase
hyphens (class-my-widget.php).

// WRONG
function getData() { ... }
$firstName = 'Jen';
class myWidget { ... }

// RIGHT
function get_data() { ... }
$first_name = 'Jen';
class My_Widget { ... }

Indentation and Braces

Use tabs for indentation, never spaces. Opening braces go on the same line for
functions, classes, and control structures. Closing braces go on their own line.

// WRONG β€” spaces, brace on new line
function get_items()
{
    if ($condition)
    {
        return true;
    }
}

// RIGHT β€” tabs, braces on same line
function get_items() {
    if ( $condition ) {
        return true;
    }
}

String Interpolation

Use single quotes when the string contains no variables. Use double quotes or
concatenation for variable strings.

// WRONG β€” double quotes with no variables
$msg = "No items found";

// RIGHT
$msg = 'No items found';
$msg = "Found {$count} items";
$msg = 'Found ' . $count . ' items';

2. WordPress-Extra Standard

WordPress-Extra extends Core with additional sniffs for code quality.

Loose Comparisons

Never use == or != for comparisons. Always use strict === or !==.

// WRONG β€” loose comparison, type coercion bugs
if ( $value == false ) { ... }
if ( $id != 0 ) { ... }

// RIGHT β€” strict comparison
if ( false === $value ) { ... }
if ( 0 !== $id ) { ... }

Assignment in Conditions

Do not assign variables inside if, while, or for conditions.

// WRONG β€” assignment in condition
if ( $result = get_post( $id ) ) { ... }

// RIGHT β€” assign first, then check
$result = get_post( $id );
if ( $result ) { ... }

Complex Expressions in Function Calls

Do not nest complex operations inside function call arguments.

// WRONG β€” complex expression inside call
update_option( 'count', absint( $_POST['count'] ) + get_option( 'offset' ) );

// RIGHT β€” compute first, then call
$count  = absint( $_POST['count'] ) + get_option( 'offset' );
update_option( 'count', $count );

Default Switch Case

Every switch statement must include a default case.

// WRONG β€” missing default
switch ( $status ) {
    case 'publish':
        break;
    case 'draft':
        break;
}

// RIGHT
switch ( $status ) {
    case 'publish':
        break;
    case 'draft':
        break;
    default:
        break;
}

3. WordPress-VIP Standard

VIP sniffs enforce performance and security for high-traffic WordPress sites.

No Direct Database Queries

Never use $wpdb->query() with inline SQL. Always use $wpdb->prepare().

// WRONG β€” direct query, SQL injection risk
$wpdb->query( "DELETE FROM {$wpdb->posts} WHERE ID = {$id}" );

// RIGHT β€” prepared statement
$wpdb->query( $wpdb->prepare( "DELETE FROM {$wpdb->posts} WHERE ID = %d", $id ) );

Restricted Functions

These functions are forbidden on VIP: eval(), extract(), create_function(),
file_put_contents(), file_get_contents() (use wp_remote_get() instead),
fwrite(), fopen() for writing, error_log() (use trigger_error() with
E_USER_WARNING), ini_set(), exec(), shell_exec(), system(),
passthru(), proc_open().

// WRONG β€” direct file operation
$data = file_get_contents( 'https://api.example.com/data' );

// RIGHT β€” WordPress HTTP API
$response = wp_remote_get( 'https://api.example.com/data' );
if ( ! is_wp_error( $response ) ) {
    $data = wp_remote_retrieve_body( $response );
}

Caching Requirements

Any function that queries the database must use object caching.

// WRONG β€” uncached query on every page load
function get_featured_ids() {
    return get_posts( array( 'fields' => 'ids', 'meta_key' => 'featured' ) );
}

// RIGHT β€” cached with expiration
function get_featured_ids() {
    $cache_key = 'featured_post_ids';
    $ids       = wp_cache_get( $cache_key, 'zentratec' );

    if ( false === $ids ) {
        $ids = get_posts( array( 'fields' => 'ids', 'meta_key' => 'featured' ) );
        wp_cache_set( $cache_key, $ids, 'zentratec', HOUR_IN_SECONDS );
    }

    return $ids;
}

4. Data Sanitization

Every piece of user input MUST be sanitized before use. Match the sanitizer to
the data type and context.

Context Sanitizer
Plain text field sanitize_text_field()
Textarea sanitize_textarea_field()
Email address sanitize_email()
URL esc_url_raw() (for DB storage)
Filename sanitize_file_name()
HTML class sanitize_html_class()
Post slug / key sanitize_key() / sanitize_title()
Integer absint() or intval()
Rich HTML (post) wp_kses_post()
Rich HTML (custom) wp_kses() with allowed tags array
Array of values array_map( 'sanitize_text_field', $arr )
// WRONG β€” raw superglobal used directly
$name = $_POST['name'];
$page = $_GET['page'];

// RIGHT β€” sanitized immediately on access
$name = sanitize_text_field( wp_unslash( $_POST['name'] ) );
$page = absint( $_GET['page'] );

Always call wp_unslash() before sanitization on superglobals because WordPress
adds magic quotes via wp_magic_quotes().


5. Data Escaping (Output)

Every value rendered in HTML MUST be escaped at the point of output. This is the
late escaping principle -- escape as late as possible, never at storage.

Output Context Escaping Function
HTML element content esc_html()
HTML attribute value esc_attr()
URL (href, src) esc_url()
JavaScript string esc_js()
Rich HTML (post body) wp_kses_post()
Translated + escaped esc_html__(), esc_attr__(), esc_html_e()
// WRONG β€” unescaped output
echo '<a href="' . $url . '">' . $title . '</a>';
echo '<input value="' . $value . '">';
_e( $user_string, 'textdomain' );

// RIGHT β€” escaped at point of output
echo '<a href="' . esc_url( $url ) . '">' . esc_html( $title ) . '</a>';
echo '<input value="' . esc_attr( $value ) . '">';
echo esc_html( $user_string );

For printf/sprintf patterns, escape within the arguments.

// RIGHT β€” escape inside sprintf arguments
printf(
    '<h2 class="%1$s">%2$s</h2>',
    esc_attr( $class ),
    esc_html( $heading )
);

6. Nonce Verification

Every form submission, AJAX request, and state-changing action MUST include and
verify a nonce to prevent CSRF attacks.

Form Pattern

// In form output:
wp_nonce_field( 'zentratec_save_settings', 'zentratec_nonce' );

// In form handler:
if ( ! isset( $_POST['zentratec_nonce'] )
    || ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['zentratec_nonce'] ) ), 'zentratec_save_settings' )
) {
    wp_die( 'Security check failed.' );
}

AJAX Pattern

// Enqueue with nonce:
wp_localize_script( 'zentratec-app', 'zentratecData', array(
    'ajax_url' => admin_url( 'admin-ajax.php' ),
    'nonce'    => wp_create_nonce( 'zentratec_ajax' ),
) );

// In AJAX handler:
function zentratec_ajax_handler() {
    check_ajax_referer( 'zentratec_ajax', 'nonce' );
    // ... process request
    wp_send_json_success( $data );
}
add_action( 'wp_ajax_zentratec_action', 'zentratec_ajax_handler' );

REST API Pattern

register_rest_route( 'zentratec/v1', '/items', array(
    'methods'             => 'POST',
    'callback'            => 'zentratec_create_item',
    'permission_callback' => function () {
        return current_user_can( 'edit_posts' );
    },
) );
// REST API uses built-in nonce via X-WP-Nonce header (wp.apiFetch handles it).

7. PHPDoc Requirements

File Header

Every PHP file must begin with a file-level docblock.

/**
 * Zentratec custom post type registration.
 *
 * @package Zentratec
 * @since   1.0.0
 */

Function Documentation

Every function must have a docblock with description, @since, @param, and
@return tags.

/**
 * Retrieve featured post IDs with caching.
 *
 * @since 1.2.0
 *
 * @param int    $count  Number of posts to return.
 * @param string $status Post status to query. Default 'publish'.
 * @return int[] Array of post IDs, empty array if none found.
 */
function zentratec_get_featured_ids( $count = 10, $status = 'publish' ) { ... }

Hook Documentation

Document do_action() and apply_filters() calls with @since and @param.

/**
 * Fires after a zentratec item is saved.
 *
 * @since 1.3.0
 *
 * @param int   $item_id   The saved item ID.
 * @param array $item_data The item data array.
 */
do_action( 'zentratec_item_saved', $item_id, $item_data );

8. Hook Callback Patterns

Named Functions Over Closures

Always use named functions for hook callbacks. Closures cannot be removed with
remove_action() / remove_filter().

// WRONG β€” anonymous closure, cannot be removed
add_action( 'init', function () {
    register_post_type( 'zt_item', array() );
} );

// RIGHT β€” named function
function zentratec_register_post_types() {
    register_post_type( 'zt_item', array() );
}
add_action( 'init', 'zentratec_register_post_types' );

Priority Management

Document non-default priorities. Use constants or comments to explain why.

// Late priority to run after other plugins register their types.
add_action( 'init', 'zentratec_modify_post_types', 99 );

Removal Pattern

To remove a callback, match the function name and priority exactly.

remove_action( 'wp_head', 'wp_generator' );
remove_filter( 'the_content', 'wpautop', 10 );

9. Database Queries

Always Prepare

Every query containing user-supplied or variable data MUST use $wpdb->prepare().

// WRONG β€” direct interpolation, SQL injection
$results = $wpdb->get_results(
    "SELECT * FROM {$wpdb->postmeta} WHERE meta_key = '{$key}' AND meta_value = '{$value}'"
);

// RIGHT β€” prepared with typed placeholders
$results = $wpdb->get_results(
    $wpdb->prepare(
        "SELECT * FROM {$wpdb->postmeta} WHERE meta_key = %s AND meta_value = %s",
        $key,
        $value
    )
);

Prefer WP Abstractions

Use WP_Query, get_posts(), get_post_meta(), get_option() instead of raw
SQL whenever possible.

// WRONG β€” raw SQL for something WP_Query handles
$wpdb->get_results( "SELECT ID FROM wp_posts WHERE post_type = 'page' LIMIT 10" );

// RIGHT β€” WordPress API
$pages = get_posts( array(
    'post_type'      => 'page',
    'posts_per_page' => 10,
    'fields'         => 'ids',
) );

When Raw SQL Is Needed

For complex queries that WP_Query cannot express, use the appropriate $wpdb
method: get_var(), get_row(), get_col(), get_results(), insert(),
update(), delete(), replace().

$total = $wpdb->get_var(
    $wpdb->prepare(
        "SELECT COUNT(*) FROM {$wpdb->posts}
         WHERE post_type = %s AND post_status = %s AND post_date > %s",
        'zt_item',
        'publish',
        $since_date
    )
);

10. Auto-Enforcement Setup

Install phpcs and WordPress-Coding-Standards

# Project-level installation with Composer
composer require --dev squizlabs/php_codesniffer
composer require --dev wp-coding-standards/wpcs
composer require --dev phpcompatibility/phpcompatibility-wp
composer require --dev dealerdirect/phpcodesniffer-composer-installer

# Verify standards are registered
vendor/bin/phpcs -i
# Should list: WordPress, WordPress-Core, WordPress-Extra, WordPress-Docs, WordPress-VIP-Go

Configuration File (phpcs.xml.dist)

Place at project root.

<?xml version="1.0"?>
<ruleset name="Zentratec">
    <description>WordPress Coding Standards for Zentratec</description>

    <file>./wp-content/themes/oshin_child/</file>
    <file>./wp-content/plugins/zentratec-*/</file>

    <exclude-pattern>*/vendor/*</exclude-pattern>
    <exclude-pattern>*/node_modules/*</exclude-pattern>

    <arg name="extensions" value="php"/>
    <arg name="colors"/>
    <arg value="sp"/>

    <config name="minimum_supported_wp_version" value="6.4"/>
    <config name="testVersion" value="8.0-"/>

    <rule ref="WordPress"/>
    <rule ref="WordPress-Docs"/>
    <rule ref="PHPCompatibilityWP"/>

    <rule ref="WordPress.NamingConventions.PrefixAllGlobals">
        <properties>
            <property name="prefixes" type="array">
                <element value="zentratec"/>
                <element value="zt_"/>
            </property>
        </properties>
    </rule>

    <rule ref="WordPress.WP.I18n">
        <properties>
            <property name="text_domain" type="array">
                <element value="zentratec"/>
            </property>
        </properties>
    </rule>
</ruleset>

Run Commands

# Check for violations
vendor/bin/phpcs

# Auto-fix what can be fixed
vendor/bin/phpcbf

# Check a single file
vendor/bin/phpcs wp-content/themes/oshin_child/functions.php

# Check with specific standard
vendor/bin/phpcs --standard=WordPress-Extra path/to/file.php

VS Code Integration

Install the wongjn.php-sniffer extension. Add to .vscode/settings.json:

{
    "phpSniffer.executablesFolder": "./vendor/bin/",
    "phpSniffer.standard": "./phpcs.xml.dist",
    "phpSniffer.autoDetect": true,
    "phpSniffer.onTypeDelay": 500
}

PhpStorm Integration

  1. Settings > PHP > Quality Tools > PHP_CodeSniffer
  2. Set path to vendor/bin/phpcs
  3. Inspections > PHP > Quality Tools > PHP_CodeSniffer validation
  4. Set coding standard to "Custom" and point to phpcs.xml.dist

CI/CD Integration (GitHub Actions)

name: WPCS
on: [pull_request]
jobs:
  phpcs:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: shivammathur/setup-php@v2
        with:
          php-version: '8.2'
          tools: composer
      - run: composer install --no-interaction
      - run: vendor/bin/phpcs --report=checkstyle | cs2pr

Quick Reference: Pre-Commit Checklist

Before committing any PHP file, verify:

  1. All user input is sanitized with the correct function
  2. All output is escaped at the point of rendering
  3. All form and AJAX handlers verify a nonce
  4. All database queries use $wpdb->prepare() or WP abstractions
  5. All functions have PHPDoc with @since, @param, @return
  6. All hook callbacks are named functions (not closures)
  7. Yoda conditions are used for all comparisons
  8. array() syntax is used (not [])
  9. Spaces inside parentheses are present
  10. Tabs are used for indentation, not spaces
  11. phpcs reports zero errors on changed files

# Supported AI Coding Agents

This skill is compatible with the SKILL.md standard and works with all major AI coding agents:

Learn more about the SKILL.md standard and how to use these skills with your preferred AI coding agent.