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:
composer require --dev "squizlabs/php_codesniffer" "wp-coding-standards/wpcs"Then configure phpcs.xml.dist in your project root:
<?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:
./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:
function get_upcoming_events() {
// Returns an array of events
}
class EventCalendar {
// Calendar display logic
}Good:
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:
// Good
add_action( 'wp_footer', 'event_manager_enqueue_footer_scripts' );
apply_filters( 'event_manager_event_query_args', $args );For constants, use uppercase with underscores:
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.
// 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:
// 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:
global $wpdb;
$result = $wpdb->get_results( "SELECT * FROM {$wpdb->postmeta} WHERE meta_key = 'color'" );
// Vulnerable to SQL injection if $color is not sanitizedGood:
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:
$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():
$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:
// 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.






Comments
Loading comments…