Skip to content
ExponaBLOG
WORDPRESS DEVELOPMENT & PLUGINS · DAY 005 / 100

WordPress coding standards: patterns, pitfalls and fixes

Write code other WordPress developers expect.

Expona Admin6 min read

Writing code other WordPress developers expect

When you join a team working on WordPress sites or plugins, the codebase you inherit often reflects years of decisions—some good, some questionable. The difference between a maintainable plugin and a tangled mess usually comes down to whether the original developers followed WordPress coding standards: patterns, pitfalls and fixes. We've all seen functions that clash with core ones, output that leaks HTML without escaping, or input validation that assumes the database is trustworthy. These aren't small style preferences; they're the building blocks of reliable, secure, and collaborative WordPress development.

PHPCS with WordPress standards

The first step toward consistent code is automation. PHP CodeSniffer (PHPCS) with the WordPress Coding Standards ruleset catches violations before code review becomes a back-and-forth about spacing and naming.

Set up PHPCS in your project by installing it via Composer:

php
composer require --dev "squizlabs/php_codesniffer" "wp-coding-standards/wpcs"

Then configure phpcs.xml.dist in your project root:

php
<?xml version="1.0"?>
<ruleset name="WordPress Project">
  <rule ref="WordPress"/>
  <rule ref="WordPress.NamingConventions.PrefixAllGlobals">
    <properties>
      <property name="prefixes" value="my_plugin"/>
    </properties>
  </rule>
  <exclude-pattern>vendor/</exclude-pattern>
  <exclude-pattern>node_modules/</exclude-pattern>
</ruleset>

Run it locally before pushing:

php
./vendor/bin/phpcs --standard=phpcs.xml.dist my-plugin/

For real-world usage, integrate PHPCS into your CI/CD pipeline. We typically run it in GitHub Actions or GitLab CI so that pull requests get flagged automatically. This shifts quality control left and prevents non-conforming code from ever reaching the main branch. The discipline pays off: after a month of enforcing standards, new code requires far less revision.

Prefix functions and classes

WordPress runs thousands of plugins and themes in the same namespace. Without proper prefixing, naming collisions are inevitable. If two plugins both define a function called save_user_data(), the second one to load will crash with a fatal error.

The rule is simple: prefix every function, class, constant, and global variable with a unique identifier tied to your plugin or theme. For a plugin called "Event Manager," use a prefix like em_ or event_manager_.

Here's a before and after:

Bad:

php
function get_upcoming_events() {
  // Returns an array of events
}

class EventCalendar {
  // Calendar display logic
}

Good:

php
function event_manager_get_upcoming_events() {
  // Returns an array of events
}

class Event_Manager_Calendar {
  // Calendar display logic
}

Apply the same logic to actions and filters:

php
// Good
add_action( 'wp_footer', 'event_manager_enqueue_footer_scripts' );
apply_filters( 'event_manager_event_query_args', $args );

For constants, use uppercase with underscores:

php
define( 'EVENT_MANAGER_VERSION', '1.0.0' );
define( 'EVENT_MANAGER_DB_VERSION', 2 );

PHPCS catches unprefixed globals automatically. Set your prefix in phpcs.xml.dist and violations will surface in code review.

Escape output, sanitize input

This is the cornerstone of WordPress security. Data flowing into your plugin or theme must be validated and cleaned; data flowing out must be escaped for its context. Mixing these up leads to SQL injection, XSS, and stored XSS vulnerabilities that attackers actively exploit.

Sanitizing input: When data comes from $_POST, $_GET, or any untrusted source, use sanitization functions to strip unwanted content.

php
// User submits a title
$title = sanitize_text_field( $_POST['event_title'] );

// A URL is provided
$url = esc_url_raw( $_POST['redirect_url'] );

// An email address
$email = sanitize_email( $_POST['contact_email'] );

// Arbitrary HTML (rare, requires careful consideration)
$description = wp_kses_post( $_POST['event_description'] );
if ( ! current_user_can( 'unfiltered_html' ) ) {
  $description = wp_kses_post( $_POST['event_description'] );
}

Escaping output: When echoing data in HTML, use context-aware escape functions:

php
// In HTML context
echo esc_html( $event_title );

// In an attribute
echo sprintf( 
  '<a href="%s">%s</a>', 
  esc_url( $event_url ), 
  esc_html( $event_name ) 
);

// In JavaScript
wp_localize_script( 'my-script', 'eventData', array(
  'title' => wp_json_encode( $event_title ),
));

// In a URL query string
echo esc_url( add_query_arg( 'id', $event_id ) );

Never use bare echo or string concatenation with user data. The cost of this discipline is negligible; the cost of a breach is not.

Use WordPress APIs before raw PHP

WordPress provides APIs for nearly everything: database queries, caching, transients, file operations, and HTTP requests. These APIs abstract away implementation details and often include security, performance, and compatibility logic you'd have to reinvent.

Database queries: Use wpdb or better yet, the REST API and custom endpoints when appropriate.

Bad:

php
global $wpdb;
$result = $wpdb->get_results( "SELECT * FROM {$wpdb->postmeta} WHERE meta_key = 'color'" );
// Vulnerable to SQL injection if $color is not sanitized

Good:

php
global $wpdb;
$result = $wpdb->get_results( 
  $wpdb->prepare( 
    "SELECT * FROM {$wpdb->postmeta} WHERE meta_key = %s", 
    'color' 
  ) 
);
// Or use get_post_meta for post-specific queries
$color = get_post_meta( $post_id, 'color', true );

Caching and transients: Store expensive results to avoid repeated computation:

php
$upcoming_events = get_transient( 'event_manager_upcoming_events' );

if ( false === $upcoming_events ) {
  $upcoming_events = event_manager_fetch_from_database();
  set_transient( 'event_manager_upcoming_events', $upcoming_events, HOUR_IN_SECONDS );
}

return $upcoming_events;

HTTP requests: Use wp_remote_get() instead of curl or file_get_contents():

php
$response = wp_remote_get( 'https://api.example.com/events', array(
  'timeout'   => 10,
  'sslverify' => true,
) );

if ( is_wp_error( $response ) ) {
  error_log( 'Event API error: ' . $response->get_error_message() );
  return array();
}

$body = wp_remote_retrieve_body( $response );
$data = json_decode( $body, true );

return $data;

This approach gives you built-in timeout handling, SSL verification, and compatibility with WordPress's HTTP stack (which respects proxy settings, etc.).

Common mistakes

Mixing escaping and sanitization: Developers sometimes escape on input, then echo raw. It works once, but when data is retrieved from the database later, it's already mangled. Sanitize on input, escape on output.

Forgetting nonces on forms: Always include a nonce when accepting POST data from the admin or frontend. This prevents CSRF attacks:

php
// In the form
wp_nonce_field( 'event_manager_save_event', 'event_nonce' );

// In the handler
if ( ! isset( $_POST['event_nonce'] ) || ! wp_verify_nonce( $_POST['event_nonce'], 'event_manager_save_event' ) ) {
  wp_die( 'Nonce verification failed' );
}

Ignoring PHPCS warnings: Some developers disable PHPCS rules they find inconvenient. This defeats the purpose. If a rule seems wrong, discuss it with the team or update the ruleset for good reason.

Assuming data is clean after retrieval: Even data stored in the database by your own code should be escaped before output. Sanitization happens once; escaping happens every time data is displayed.

Key takeaways

  • Run PHPCS with WordPress standards in CI/CD to catch style and security violations automatically.
  • Prefix all functions, classes, constants, and globals with a unique identifier to prevent collisions.
  • Sanitize all input from $_POST, $_GET, and external sources; escape all output for its context.
  • Use WordPress APIs (wpdb::prepare(), transients, wp_remote_get()) instead of raw PHP to gain security and compatibility.
  • Include nonces on forms to prevent CSRF attacks.
  • Treat PHPCS and code standards as non-negotiable, not optional polish.
ShareXLinkedInReddit

Found this useful? Support the writing.

Buy me a coffee
Written by
Expona Admin
Site administrator

Comments

Loading comments…

Leave a comment

Comments are reviewed before they appear. Be kind and stay on topic.

Related articles