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.
- Use the WordPress HTTP API over Guzzle or other libraries.
- Use the WordPress Filesystem API over
file_get_contents()
. - Use
wp_json_encode()
overjson_encode()
.
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.
- When working with a Post, Page or Custom Post Type, pass an integer ID or object to
get_post_type()
to assure a string response with thepost_type
. - When working a user, check
if ( $user instance of WP_User )
. - When working with object keys, use
property_exists()
, particularly when it’s conditionally available. This may require checking a few levels in a multidimensional object. - When working with arrays, use
isset( $array['key'] )
or `isset( $array[ X ] ) prior to referencing a key or positional item. This may require checking a few levels in a multidimensional array.
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:
WP_Query
WP_User_Query
WP_Comment_Query
WP_Term_Query
WP_Tax_Query
WP_Date_Query
WP_Meta_Query
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
- Never use unbounded/unlimited queries (i.e.
posts_per_page => -1
). - Limit fields with
fields
arguments, particularly for large queries. - Disable meta and term caches when they’re not necessary.
- Use
no_found_rows => true
for unpaginated queries. - Avoid multi-dimensional queries (
{term}__and
and{term}__not_in
) and queries that require multiple table lookups via joins.
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 );
}
}