Skip to the content.

WordPress PHP

WordPress has evolved a great deal since May 2003, both maintaining backwards compatibility and leveraging new PHP language features as they became available.

A variety of coding styles and approaches developed that act as witness marks for the era they were added.

Our standards recognize this same evolution in our own codebases, while expecting higher quality and uniformity in new and updated code.

Quick Start

Our PHPCS ruleset extends common WordPress standards.

1. Require the Newfold Labs Satis Repository

composer config repository https://newfold-labs.github.io/satis

2. Require the PHP Standards

composer require --dev newfold-labs/phpcs-wp-standards

3. Run the PHP Linter (add to Composer Scripts as “lint”)

./vendor/bin/phpcs .

4. Run the automatic formatter (add to Composer Scripts as “fix”)

./vendor/bin/phpcbf .

Minimum Supported Versions

See Minimum Supported Versions > PHP.

Prefer Namespaced Methods, Constants and Classes

While in some situations use of the global namespace is required or sensible, our default is namespaced PHP following PSR-4 conventions for autoloading.

We expect our WordPress code to follow Newfold\WP\{Plugin|Theme|Module}\{Name}.

Examples

<?php
namespace Newfold\WP\Module\Staging;
namespace Newfold\WP\Plugin\Bluehost;
namespace Newfold\WP\Plugin\HostGator;

NOTE: When a brand name has explicit letter casing, like WordPress, HostGator or WooCommerce, we always aim to preserve this casing for consistiency and do not change case to Wordpress, Hostgator or Woocommerce.

Instantiation & Autoloading

We use Composer to autoload PHP files. We prefer PSR-4 patterns, classmaps and file references, in that order.

Keep in mind WordPress executes code from within it’s system of hooks and filters, that Plugins are executed in numeric-alpha order and that most of our code should leverage hooks and priorities to avoid inconsistiencies and allow filterability.

Finally, when manually referencing PHP files, use include_once and require_once unless specifically leveraging the benefits of include and require.

Prefixing in Global Namespaces

Any global functions, classes or constants should be prefixed with NFD in the proper kebab or snake case for the context.

_NOTE: This only applies to code in the global namespace. Code within our namespace should NOT be prefixed, only things like cache and database keys.

<?php

define('NFD_PLATFORM_BRAND', 'bluehost');

apply_filters('nfd_platform_branding', 'bluehost');

wp_register_script('nfd_module_admin_widget', // ... )

function nfd_get_platform() { 
    // ... 
}

class NFD_Platform() { 
    // ... 
}

Main plugin file matches project name

While WordPress supports the ability to have the main plugin file not match the directory name, we expect the main plugin file name to mirror the directory.

❌ wp-plugin-bluehost/bluehost.php
❌ wp-plugin-bluehost/main.php
✅ wp-plugin-bluehost/wp-plugin-bluehost.php
✅ bluehost-maestro/bluehost-maestro.php

Use bootstrap.php in Themes, Plugins and modules to isolate runtime from application scaffolding.

WordPress Plugins have Plugin Headers in their main file, WordPress Themes expect all PHP to execute from functions.php, but we try not to put the primary application scaffolding or code instantiation in these files.

Instead to contain most code to a bootstrap.php.

Assure loading from within WordPress

Source files should be protected from having source viewed on the server. We use a constant WordPress defines like WPINC or ABSPATH to confirm it’s WordPress loading the source and not an HTTP request in a browser or terminal.

// Do not access file directly!
if ( ! defined( 'WPINC' ) ) {
	die;
}

Set PHP constants in the global namespace

Setting these constants in the main file assures the codebase always has access to them. Many WordPress methods require a version for cache-busting, a URL for referencing the absolute path or URL to assets or need to know the filepath to the main plugin file (ex: activation and deactivation hooks).

Subsequent code should be namespaced, but the primary file should execute in the global namespace.

define( '{PRODUCT}_VERSION', '1.0.0' );
define( '{PRODUCT}_URL', '' );
define( '{PRODUCT}_DIR', __DIR__ );
define( '{PRODUCT}_FILE', __FILE__ );

Composer Autoloading

We include Composer’s autoloader here, so we know everything is available before continuing execution.

if ( is_readable( PLUGIN_DIR . '/vendor/autoload.php' ) ) {
    require_once PLUGIN_DIR . '/vendor/autoload.php';
} else {
    // Also a good opportunity for an Admin Notice or WP_Error to indicate this failed.
    return;
}

Minimum version checks

We might run compatibility code within the bootstrap.php, but if subsequent code will cause a fatal error in some versions of PHP or WordPress, we include those checks as preconditions to requiring bootstrap.php.

global $wp_db_version;
if ( 
    version_compare( PHP_VERSION, '5.6', '>=' ) 
    && version_compare( $wp_version, '22441', '>=' ) 
) {
    require_once PLUGIN_DIR . '/bootstrap.php';
}

Leverage Core-supplied solutions over PHP-native and custom solutions

WordPress provides numerous internal APIs and methods that are intended to supercede their PHP equivalents. We leverage these solutions whenever possible to benefit from the broad environment support, security-hardening and proven-in-production peace of mind they provide.

Assume PHP that executes on the frontend may get cached

Never assume PHP will get executed on every pageload. Always assume that Newfold or third-party vendor caching may burn markup into cache stores.

One common pain point is codebases that print WordPress Nonces in the DOM. Many caching solutions use nonces to set a cache miss, or worse burn them in and they exceed their expiry timestamp. In general, nonces should be reserved for logged-in WordPress users or used on the frontend with the expectation error states are necessary and reliability can be an issue.

Use defensive checks when using files, executing code and referencing keys within arrays and objects.

When requiring or including files, is_readable() should be checked to avoid fatal errors. is_readable() is preferred to file_exists() and is_dir() because it goes the extra step of making sure permissions are correct for the resource.

When instantiating external classes or methods that may not always be available, use is_callable(). is_callable() is preferred to function_exists() and class_exists() because it goes the extra step of making sure the callback doesn’t just exist but can be executed.

This is important whenever writing code that interacts with other Newfold codebases, WooCommerce, Jetpack and code that may conditionally be available.

When referencing object keys we often need to use a method to check a WordPress Core object is correctly formed or that a property exists before referencing it.

Explicitly declare visibility of object properties and methods

Any callback provided to the WordPress Action Hooks and Filter system must be a public method.

However, those public callbacks can leverage protected and private methods, so as a general best practice, we use PHP visibility to document intention and good code hygene.

Otherwise, we default to private visibility, stepping up to protected for objects intended to be extended.

Use strictly-typed checks where possible

Whenever possible, type checks should be strict === vs ==.

WordPress methods don’t always return strict types – such as Options – and in that case typecasting should be leveraged.

in_array() isn’t preferred if isset() can suffice for key checks, but when in_array() is used the strict parameter should be set whenever possible.

Use the WordPress Transients API over direct-access to caching methods

When an object cache is in use, the Transients API handles all setting of cache keys behind-the-scenes. But when an object cache is unavailable, the Transients API gracefully falls back to the wp_options table.

Avoid heredoc and nowdoc syntax

While heredoc and nowdoc syntaxes can make for easier-to-read PHP, there are edge-cases with them in different environments that can result in fatal errors.

As a general rule we avoid heredoc and nowdoc because the slight benefits do not outweigh the risk when the edgecases go south.

Use ?rest_route instead of /wp-json in API requests

To assure our REST API requests work more often, we prefer domain.com?rest_route=/wp/v2/posts to domain.com/wp-json/v2/posts.

This accounts for situations where WordPress’ permalinks have not been set, where the REST prefix (/wp-json) has been customized and when there are nonstandard structures like domain.com/index.php/wp-json.

Avoid excessive use of output buffers and always flush them

PHP output buffers can sometimes be a great tool in a developers’ toolbelt, but failing to flush a buffer can have undesirable results so we expect ob_get_clean(), ob_get_flush() or other methods of emptying the output buffer to be used.

Don’t use PHP sessions

This is a hard rule. Never, under any circumstances, leverage PHP Sessions in WordPress codebases. There are substantial security and experiences issues to overcome even when you control the environment completely and we do not.

Aim for efficient database queries

WordPress supplies a number of performance-optimized, cache-supported, security-conscious objects to query data in the MySQL database including:

We lean on these queries for security hardening and the caching benefits they enable in lieu of other Core methods like get_posts() and get_users().

Narrowly scope WP_Query and internal queries

Aim for single-purpose functions

In procedural code like WordPress, it’s easy to create sequential logic in a single function.

One of the best ways to make reusable code is putting tasks into helper functions that get composed into a sequence.

When a single method is used to read data, and another to write, instead of a half-dozen calls to the same WordPress Option, it makes future iteration, data migration and edge-cases easier to address as the logic happens in one or two places, instead of needing to be duplicated into a half-dozen or dozen places.

// This method could be split into helper methods that can help reduce repetitive code
// as the application grows and evolves.
function get_data() {
    $data = get_transient('nfd_cached_response');
    if ( false === $data ) {
        $raw_response = wp_remote_get('https://domain.com/api/resource');
        if ( 200 === wp_remote_retrieve_response_code( $raw_response ) ) {
            $data = wp_remote_retrieve_response_body( $raw_response );
            if ( is_array( $array = json_decode( $data, true ) ) ) {
                set_transient('nfd_cached_response', $array, 20 * MINUTE_IN_SECONDS );
            }
        }
    }

    return $data;
}
// instead, isolating the logic allows for more robust and reusable code
class Cached_API_Data {
    public static $key = 'nfd_cached_response';
    public static $endpoint_path = 'https://domain.com/api/resource';

    public static function get() {
        $data = get_transient( static::$key );
        if ( false === $data ) {
            $data = static::remote_request();
            // if is_wp_error() retry X times, etc.
        }

        return $data;
    }

    public static function remote_request() {
        $raw_response = wp_remote_get(static::$endpoint_path);
        if ( ! is_wp_error( $raw_response ) && 200 === wp_remote_retrieve_response_code( $raw_response ) ) {
            $data = wp_remote_retrieve_response_body( $raw_response );
            if ( is_array( $array = json_decode( $data, true ) ) ) {
                static::cache( $array );
                return $array;
            }
        }

        return new \WP_Error( 'slug-for-error', __('Error message', 'text-domain') );
    }

    public static function cache( $data, $duration = 20 * MINUTE_IN_SECONDS ) {
        set_transient( static::$key, $data, $duration );
    }

    public static function delete() {
        delete_transient( static::$key );
    }
}