WordPress Development

WordPress Coding Standards: A Practical Review Checklist

What to check when reviewing WordPress code: escaping, sanitisation, capability checks, queries, hooks and the structural choices that decide maintainability.

By 6 min read
Code editor window with syntax-highlighted code

Code review on WordPress projects often stalls on brace placement while a missing capability check goes through. The formatting matters, but a linter can enforce it — a human should be looking at the things a linter can't see.

This is the checklist I work through, roughly in order of how much damage each item can do.

Automate what can be automated

Run PHP_CodeSniffer with the WordPress ruleset in CI, and stop discussing formatting in reviews. The standard covers indentation, naming, spacing, documentation blocks and a useful set of security checks — unescaped output and unsanitised input among them.

Add a PHP compatibility check against your minimum supported version too, which catches syntax and functions that will fatal on an older server.

Once those run automatically, review conversations move to the things that actually need judgement.

Security: escaping output

  • Is every dynamic value escaped at the point of output? esc_html for text, esc_attr for attributes, esc_url for URLs, wp_kses_post for content that may legitimately contain markup.
  • Escape late — at output, not when storing. Data that was safe when stored may not be safe in the context it's finally used.
  • Is anything being echoed without escaping because 'we put it there ourselves'? That's the assumption most XSS holes are built on.
  • Are translated strings escaped? esc_html__ rather than __ followed by a raw echo.
  • Is JSON output through wp_json_encode rather than hand-built?

Security: input and permissions

  • Is every value from a request sanitised with a function appropriate to its type, not just run through a generic one?
  • Is it validated as well as sanitised? Sanitising makes a value safe; validating confirms it's the right value.
  • Is there a nonce on every form and AJAX action that changes something, and is it verified server-side?
  • Is there a capability check with current_user_can? A nonce proves intent, not permission — you need both.
  • For actions on a specific object, is the capability checked against that object's ID?
  • Does any custom SQL use $wpdb->prepare? And could it use a WordPress API instead of custom SQL at all?
  • Are file uploads restricted by type, and stored somewhere PHP can't execute?

Queries and performance

  • Any queries inside a loop? That's an N+1 problem and it's the most common performance bug in WordPress code.
  • Is posts_per_page bounded, or could it be -1 on a table that grows?
  • Are no_found_rows and fields used where the full result isn't needed?
  • Are meta queries being used where a taxonomy would be faster?
  • Is anything making a remote HTTP request during a page render? That blocks PHP until the remote server answers.
  • Are expensive results cached in a transient, with invalidation on the event that makes them stale?
  • Are assets enqueued conditionally, or loaded on every page regardless of need?

Hooks

  • Is the hook the right one? Registering post types on init, not plugins_loaded. Not using conditional tags before the query has run.
  • Do filter callbacks always return a value on every code path? A filter returning nothing is the most common hook bug there is.
  • Is priority set deliberately where order matters, rather than relying on luck?
  • Are callbacks named and referenced properly, so they can be removed by other code? An anonymous function attached to a hook can never be unhooked.
  • Is anything hooked that runs on every request but is only needed in one context?

Structure: is it in the right place?

This is the question that decides whether a site is maintainable in three years, and it's the one a linter cannot ask.

  • Is anything in the theme that should be in a plugin? Custom post types, shortcodes, integrations, business logic — all of it disappears or breaks on a theme change.
  • Is the theme's functions.php a single enormous file, or does it require focused files from an includes directory?
  • Is logic separated from presentation, so templates contain markup rather than queries?
  • Is anything duplicated that should be a shared function or template part?
  • Are things named for what they are rather than how they currently look?

Naming and collisions

  • Is everything prefixed or namespaced? Unprefixed function names in the global scope will collide eventually.
  • Are option names, meta keys, post type keys and taxonomy slugs prefixed too?
  • Are meta keys intended to be internal prefixed with an underscore so they don't show in the custom fields UI?
  • Are class names, hooks and constants consistently named across the codebase?

Internationalisation and accessibility

  • Are user-facing strings wrapped in translation functions with a consistent text domain?
  • Do strings with placeholders use printf-style formatting rather than concatenation, so translators can reorder?
  • Do generated form fields have associated labels?
  • Do images output by templates have alt attributes, with a way for editors to set them?
  • Are interactive elements actual buttons and links rather than divs with click handlers?
  • Does the markup use heading levels in order?

Documentation that earns its place

Comments explaining what a line does are noise; the code says that already. Comments explaining why are the ones worth writing, and they are the ones missing from almost every WordPress codebase I inherit.

  • Does a non-obvious workaround say what it works around? A line that exists because of a plugin bug is incomprehensible once the bug is fixed and forgotten.
  • Do functions with several parameters have a docblock saying what each is and what comes back?
  • Is there a note where an integration expects a particular remote response shape, so the next person knows what breaks it?
  • Does the repository have a readme covering local setup, the deploy process and where configuration lives?
  • Are magic values named? A bare number in a condition is a question nobody can answer in six months.

The things reviewers forget to check

  • What happens on a fresh install with no content? Empty states are where template code throws notices.
  • What happens when an expected custom field is missing, which is what old content looks like after a new field is added?
  • Are errors handled, or does the code assume every API call and query succeeds?
  • Is anything logging personal data?
  • Does the code work with the site in a subdirectory, and with HTTPS?
  • Has anyone actually run it with WP_DEBUG on? Notices and deprecations are where tomorrow's fatal errors are visible today.

Frequently asked questions

Do I have to follow the WordPress coding standards exactly?

For plugins and themes meant for distribution, yes — reviewers expect it and so does everyone who reads the code. For a private site, pick a standard and apply it consistently. The WordPress standard is the sensible default simply because every WordPress developer already reads it fluently.

What's the most important thing to check in a WordPress code review?

Escaping on output and capability checks on anything that changes state. Those two account for most of the vulnerabilities that actually get exploited. After that, whether the code is in the right place — theme versus plugin — because that decides maintainability more than anything else.

How do I set up PHPCS for WordPress?

Install PHP_CodeSniffer with the WordPress Coding Standards ruleset, add a configuration file to the repository so everyone uses the same rules, and run it in CI so it can't be skipped. Add a PHP compatibility check against your minimum supported version at the same time.

Is it acceptable to put custom code in functions.php?

For presentation — enqueuing the theme's assets, template tweaks, display logic — yes, that's what it's for. For custom post types, shortcodes, integrations or business logic, no: a theme change would break them. Put those in a site-specific plugin, which costs nothing to create.

Why does my filter callback break the site?

Most often because it doesn't return a value on every code path. A filter passes a value through your callback and uses what you return — an early return with nothing, or a conditional that falls through, hands WordPress null instead of the value. It's the single most common hook bug.

Topics

  • WordPress coding standards
  • WordPress code review
  • PHPCS WordPress
  • WordPress best practices