PHP

Performance

Writing performant code is absolutely critical, especially at the enterprise level. There are a number of strategies and best practices we must employ to ensure our code is optimized for high-traffic situations.
There are drastically different constraints when developing a high-traffic Enterprise-scale WordPress site as opposed to a site for a small business hosted on a shared server. High-performance WordPress code imposes additional constraints on what you can and cannot do, and forces you to find optimization paths for your code. A lot of good resources for getting up to speed on Enterprise-scale WordPress development can be found on the WordPress.com VIP docs, since VIP is focused on very high scale WordPress sites.

Efficient Database Queries

When querying the database in WordPress, you should generally use a WP_Query object. WP_Queryobjects take a number of useful arguments and do things behind-the-scenes that other database access methods such as get_posts() do not.
Here are a few key points:
    Only run the queries that you need.
    A new WP_Query object runs five queries by default, including calculating pagination and priming the term and meta caches. Each of the following arguments will remove a query:
      'no_found_rows' => true: useful when pagination is not needed.
      'update_post_meta_cache' => false: useful when post meta will not be utilized.
      'update_post_term_cache' => false: useful when taxonomy terms will not be utilized.
      'fields' => 'ids': useful when only the post IDs are needed (less typical).
    Do not use posts_per_page => -1.
    This is a performance hazard. What if we have 100,000 posts? This could crash the site. If you are writing a widget, for example, and just want to grab all of a custom post type, determine a reasonable upper limit for your situation.
    1
    <?php
    2
    // Query for 100 posts.
    3
    new WP_Query( array(
    4
    'posts_per_page' => 100,
    5
    ));
    6
    ?>
    Copied!
    Do not use $wpdb or get_posts() unless you have good reason.
    get_posts() actually calls WP_Query, but calling get_posts() directly bypasses a number of filters by default. Not sure whether you need these things or not? You probably don’t.
    If you don’t plan to paginate query results, always pass no_found_rows => true to WP_Query.
    This will tell WordPress not to run SQL_CALC_FOUND_ROWS on the SQL query, drastically speeding up your query. SQL_CALC_FOUND_ROWS calculates the total number of rows in your query which is required to know the total amount of “pages” for pagination.
    1
    <?php
    2
    // Skip SQL_CALC_FOUND_ROWS for performance (no pagination).
    3
    new WP_Query( array(
    4
    'no_found_rows' => true,
    5
    ));
    6
    ?>
    Copied!
    Avoid using post__not_in.
    In most cases it’s quicker to filter out the posts you don’t need in PHP instead of within the query. This also means it can take advantage of better caching. This won’t work correctly (without additional tweaks) for pagination.
    Use :
    1
    <?php
    2
    $foo_query = new WP_Query( array(
    3
    'post_type' => 'post',
    4
    'posts_per_page' => 30 + count( $posts_to_exclude )
    5
    ) );
    6
    7
    if ( $foo_query->have_posts() ) :
    8
    while ( $foo_query->have_posts() ) :
    9
    $foo_query->the_post();
    10
    if ( in_array( get_the_ID(), $posts_to_exclude ) ) {
    11
    continue;
    12
    }
    13
    the_title();
    14
    endwhile;
    15
    endif;
    16
    ?>
    Copied!
    Instead of:
    1
    <?php
    2
    $foo_query = new WP_Query( array(
    3
    'post_type' => 'post',
    4
    'posts_per_page' => 30,
    5
    'post__not_in' => $posts_to_exclude
    6
    ) );
    7
    ?>
    Copied!
    A taxonomy is a tool that lets us group or classify posts.
    Post meta lets us store unique information about specific posts. As such the way post meta is stored does not facilitate efficient post lookups. Generally, looking up posts by post meta should be avoided (sometimes it can’t). If you have to use one, make sure that it’s not the main query and that it’s cached.
    Passing cache_results => false to WP_Query is usually not a good idea.
    If cache_results => true (which is true by default if you have caching enabled and an object cache setup), WP_Query will cache the posts found among other things. It makes sense to use cache_results => false in rare situations (possibly WP-CLI commands).
    Multi-dimensional queries should be avoided.
    Examples of multi-dimensional queries include:
    1
    * Querying for posts based on terms across multiple taxonomies
    2
    * Querying multiple post meta keys
    Copied!
    Each extra dimension of a query joins an extra database table. Instead, query by the minimum number of dimensions possible and use PHP to filter out results you don’t need.
    Here is an example of a 2-dimensional query:
    1
    <?php
    2
    // Query for posts with both a particular category and tag.
    3
    new WP_Query( array(
    4
    'category_name' => 'cat-slug',
    5
    'tag' => 'tag-slug',
    6
    ));
    7
    ?>
    Copied!

WP_Query vs. get_posts() vs. query_posts()

As outlined above, get_posts() and WP_Query, apart from some slight nuances, are quite similar. Both have the same performance cost (minus the implication of skipping filters): the query performed.
query_posts(), on the other hand, behaves quite differently than the other two and should almost never be used. Specifically:
    It creates a new WP_Query object with the parameters you specify.
    It replaces the existing main query loop with a new instance of WP_Query.
As noted in the WordPress Codex (along with a useful query flow chart), query_posts() isn’t meant to be used by plugins or themes. Due to replacing and possibly re-running the main query, query_posts() is not performant and certainly not an acceptable way of changing the main query.

Build arrays that encourage lookup by key instead of search by value

in_array() is not an efficient way to find if a given value is present in an array. The worst case scenario is that the whole array needs to be traversed, thus making it a function with O(n) complexity. VIP review reports in_array() use as an error, as it’s known not to scale.
The best way to check if a value is present in an array is by building arrays that encourage lookup by key and use isset(). isset() uses an O(1) hash search on the key and will scale.
Here is an example of an array that encourages lookup by key by using the intended values as keys of an associative array
1
<?php
2
$array = array(
3
'foo' => true,
4
'bar' => true,
5
);
6
if ( isset( $array['bar'] ) ) {
7
// value is present in the array
8
};
Copied!
In case you don’t have control over the array creation process and are forced to use in_array(), to improve the performance slightly, you should always set the third parameter to true to force use of strict comparison.

Caching

Caching is simply the act of storing computed data somewhere for later use, and is an incredibly important concept in WordPress. There are different ways to employ caching, and often multiple methods will be used.

The “Object Cache”

Object caching is the act of caching data or objects for later use. In the context of WordPress, objects are cached in memory so they can be retrieved quickly.
In WordPress, the object cache functionality provided by WP_Object_Cache, and the Transients API are great solutions for improving performance on long-running queries, complex functions, or similar.
On a regular WordPress install, the difference between transients and the object cache is that transients are persistent and would write to the options table, while the object cache only persists for the particular page load.
It is possible to create a transient that will never expire by omitting the third parameter, this should be avoided as any non-expiring transients are autoloaded on every page and you may actually decrease performance by doing so.
On environments with a persistent caching mechanism (i.e. Memcache, Redis, or similar) enabled, the transient functions become wrappers for the normal WP_Object_Cache functions. The objects are identically stored in the object cache and will be available across page loads.
High-traffic environments not using a persistent caching mechanism should be wary of using transients and filling the wp_options table with an excessive amount of data. See the “Appropriate Data Storage” section for details.
Note: as the objects are stored in memory, you need to consider that these objects can be cleared at any time and that your code must be constructed in a way that it would not rely on the objects being in place.
This means you always need to ensure you check for the existence of a cached object and be ready to generate it in case it’s not available. Here is an example:
1
<?php
2
/**
3
* Retrieve top 10 most-commented posts and cache the results.
4
*
5
* @return array|WP_Error Array of WP_Post objects with the highest comment counts,
6
* WP_Error object otherwise.
7
*/
8
function prefix_get_top_commented_posts() {
9
// Check for the top_commented_posts key in the 'top_posts' group.
10
$top_commented_posts = wp_cache_get( 'prefix_top_commented_posts', 'top_posts' );
11
12
// If nothing is found, build the object.
13
if ( false === $top_commented_posts ) {
14
// Grab the top 10 most commented posts.
15
$top_commented_posts = new WP_Query( 'orderby=comment_count&posts_per_page=10' );
16
17
if ( ! is_wp_error( $top_commented_posts ) && $top_commented_posts->have_posts() ) {
18
// Cache the whole WP_Query object in the cache and store it for 5 minutes (300 secs).
19
wp_cache_set( 'prefix_top_commented_posts', $top_commented_posts->posts, 'top_posts', 5 * MINUTE_IN_SECONDS );
20
}
21
}
22
return $top_commented_posts;
23
}
24
?>
Copied!
In the above example, the cache is checked for an object with the 10 most commented posts and would generate the list in case the object is not in the cache yet. Generally, calls to WP_Query other than the main query should be cached.
As the content is cached for 300 seconds, the query execution is limited to one time every 5 minutes, which is nice.
However, the cache rebuild in this example would always be triggered by a visitor who would hit a stale cache, which will increase the page load time for the visitors and under high-traffic conditions. This can cause race conditions when a lot of people hit a stale cache for a complex query at the same time. In the worst case, this could cause queries at the database server to pile up causing replication, lag, or worse.
That said, a relatively easy solution for this problem is to make sure that your users would ideally always hit a primed cache. To accomplish this, you need to think about the conditions that need to be met to make the cached value invalid. In our case this would be the change of a comment.
The easiest hook we could identify that would be triggered for any of this actions would be wp_update_comment_count set as do_action( 'wp_update_comment_count', $post_id, $new, $old ).
With this in mind, the function could be changed so that the cache would always be primed when this action is triggered.
Here is how it’s done:
1
<?php
2
/**
3
* Prime the cache for the top 10 most-commented posts.
4
*
5
* @param int $post_id Post ID.
6
* @param int $new The new comment count.
7
* @param int $old The old comment count.
8
*/
9
function prefix_refresh_top_commented_posts( $post_id, $new, $old ) {
10
// Force the cache refresh for top-commented posts.
11
prefix_get_top_commented_posts( $force_refresh = true );
12
}
13
add_action( 'wp_update_comment_count', 'prefix_refresh_top_commented_posts', 10, 3 );
14
15
/**
16
* Retrieve top 10 most-commented posts and cache the results.
17
*
18
* @param bool $force_refresh Optional. Whether to force the cache to be refreshed. Default false.
19
* @return array|WP_Error Array of WP_Post objects with the highest comment counts, WP_Error object otherwise.
20
*/
21
function prefix_get_top_commented_posts( $force_refresh = false ) {
22
// Check for the top_commented_posts key in the 'top_posts' group.
23
$top_commented_posts = wp_cache_get( 'prefix_top_commented_posts', 'top_posts' );
24
25
// If nothing is found, build the object.
26
if ( true === $force_refresh || false === $top_commented_posts ) {
27
// Grab the top 10 most commented posts.
28
$top_commented_posts = new WP_Query( 'orderby=comment_count&posts_per_page=10' );
29
30
if ( ! is_wp_error( $top_commented_posts ) && $top_commented_posts->have_posts() ) {
31
// In this case we don't need a timed cache expiration.
32
wp_cache_set( 'prefix_top_commented_posts', $top_commented_posts->posts, 'top_posts' );
33
}
34
}
35
return $top_commented_posts;
36
}
37
?>
Copied!
With this implementation, you can keep the cache object forever and don’t need to add an expiration for the object as you would create a new cache entry whenever it is required. Just keep in mind that some external caches (like Memcache) can invalidate cache objects without any input from WordPress.
For that reason, it’s best to make the code that repopulates the cache available for many situations.
In some cases, it might be necessary to create multiple objects depending on the parameters a function is called with. In these cases, it’s usually a good idea to create a cache key which includes a representation of the variable parameters. A simple solution for this would be appending an md5 hash of the serialized parameters to the key name.

Page Caching

Page caching in the context of web development refers to storing a requested location’s entire output to serve in the event of subsequent requests to the same location.
Batcache is a WordPress plugin that uses the object cache (often Memcache in the context of WordPress) to store and serve rendered pages. It can also optionally cache redirects. It’s not as fast as some other caching plugins, but it can be used where file-based caching is not practical or desired.
Batcache is aimed at preventing a flood of traffic from breaking your site. It does this by serving old (5 minute max age by default, but adjustable) pages to new users. This reduces the demand on the web server CPU and the database. It also means some people may see a page that is a few minutes old. However, this only applies to people who have not interacted with your website before. Once they have logged-in or left a comment, they will always get fresh pages.
Although this plugin has a lot of benefits, it also has a couple of code design requirements:
    As the rendered HTML of your pages might be cached, you cannot rely on server side logic related to $_SERVER, $_COOKIE or other values that are unique to a particular user.
    You can however implement cookie or other user based logic on the front-end (e.g. with JavaScript)
Batcache does not cache logged in users (based on WordPress login cookies), so keep in mind the performance implications for subscription sites (like BuddyPress). Batcache also treats the query string as part of the URL which means the use of query strings for tracking campaigns (common with Google Analytics) can render page caching ineffective. Also beware that while WordPress VIP uses batcache, there are specific rules and conditions on VIP that do not apply to the open source version of the plugin.
There are other popular page caching solutions such as the W3 Total Cache plugin, though we generally do not use them for a variety of reasons.

AJAX Endpoints

AJAX stands for Asynchronous JavaScript and XML. Often, we use JavaScript on the client-side to ping endpoints for things like infinite scroll.
WordPress provides an API to register AJAX endpoints on wp-admin/admin-ajax.php. However, WordPress does not cache queries within the administration panel for obvious reasons. Therefore, if you send requests to an admin-ajax.php endpoint, you are bootstrapping WordPress and running un-cached queries. Used properly, this is totally fine. However, this can take down a website if used on the frontend.
For this reason, front-facing endpoints should be written by using the Rewrite Rules API and hooking early into the WordPress request process.
Here is a simple example of how to structure your endpoints:
1
<?php
2
/**
3
* Register a rewrite endpoint for the API.
4
*/
5
function prefix_add_api_endpoints() {
6
add_rewrite_tag( '%api_item_id%', '([0-9]+)' );
7
add_rewrite_rule( 'api/items/([0-9]+)/?', 'index.php?api_item_id=$matches[1]', 'top' );
8
}
9
add_action( 'init', 'prefix_add_api_endpoints' );
10
11
/**
12
* Handle data (maybe) passed to the API endpoint.
13
*/
14
function prefix_do_api() {
15
global $wp_query;
16
17
$item_id = $wp_query->get( 'api_item_id' );
18
19
if ( ! empty( $item_id ) ) {
20
$response = array();
21
22
// Do stuff with $item_id
23
24
wp_send_json( $response );
25
}
26
}
27
add_action( 'template_redirect', 'prefix_do_api' );
28
?>
Copied!

Cache Remote Requests

Requests made to third-parties, whether synchronous or asynchronous, should be cached. Not doing so will result in your site’s load time depending on an unreliable third-party!
Here is a quick code example for caching a third-party request:
1
<?php
2
/**
3
* Retrieve posts from another blog and cache the response body.
4
*
5
* @return string Body of the response. Empty string if no body or incorrect parameter given.
6
*/
7
function prefix_get_posts_from_other_blog() {
8
if ( false === ( $posts = wp_cache_get( 'prefix_other_blog_posts' ) ) {
9
10
$request = wp_remote_get( ... );
11
$posts = wp_remote_retrieve_body( $request );
12
13
wp_cache_set( 'prefix_other_blog_posts', $posts, '', HOUR_IN_SECONDS );
14
}
15
return $posts;
16
}
17
?>
Copied!
prefix_get_posts_from_other_blog() can be called to get posts from a third-party and will handle caching internally.

Appropriate Data Storage

Utilizing built-in WordPress APIs we can store data in a number of ways.
We can store data using options, post meta, post types, object cache, and taxonomy terms.
There are a number of performance considerations for each WordPress storage vehicle:
    Options - The options API is a simple key-value storage system backed by a MySQL table. This API is meant to store things like settings and not variable amounts of data.
    Site performance, especially on large websites, can be negatively affected by a large options table. It’s recommended to regularly monitor and keep this table under 500 rows. The “autoload” field should only be set to ‘yes’ for values that need to be loaded into memory on each page load.
    Caching plugins can also be negatively affected by a large wp_options table. Popular caching plugins such as Memcached place a 1MB limit on individual values stored in cache. A large options table can easily exceed this limit, severely slowing each page load.
    Post Meta or Custom Fields - Post meta is an API meant for storing information specific to a post. For example, if we had a custom post type, “Product”, “serial number” would be information appropriate for post meta. Because of this, it usually doesn’t make sense to search for groups of posts based on post meta.
    Taxonomies and Terms - Taxonomies are essentially groupings. If we have a classification that spans multiple posts, it is a good fit for a taxonomy term. For example, if we had a custom post type, “Car”, “Nissan” would be a good term since multiple cars are made by Nissan. Taxonomy terms can be efficiently searched across as opposed to post meta.
    Custom Post Types - WordPress has the notion of “post types”. “Post” is a post type which can be confusing. We can register custom post types to store all sorts of interesting pieces of data. If we have a variable amount of data to store such as a product, a custom post type might be a good fit.
    Object Cache - See the “Caching” section.
While it is possible to use WordPress’ Filesystem API to interact with a huge variety of storage endpoints, using the filesystem to store and deliver data outside of regular asset uploads should be avoided as this methods conflict with most modern / secure hosting solutions.

Database Writes

Writing information to the database is at the core of any website you build. Here are some tips:
    Generally, do not write to the database on frontend pages as doing so can result in major performance issues and race conditions.
    When multiple threads (or page requests) read or write to a shared location in memory and the order of those read or writes is unknown, you have what is known as a race condition.
    Store information in the correct place. See the “Appropriate Data Storage” section.
    Certain options are “autoloaded” or put into the object cache on each page load. When creating or updating options, you can pass an $autoload argument to add_option(). If your option is not going to get used often, it shouldn’t be autoloaded. As of WordPress 4.2, update_option()supports configuring autoloading directly by passing an optional $autoload argument. Using this third parameter is preferable to using a combination of delete_option() and add_option() to disable autoloading for existing options.

Design Patterns

Using a common set of design patterns while working with PHP code is the easiest way to ensure the maintainability of a project. This section addresses standard practices that set a low barrier for entry to new developers on the project.

Namespacing

We properly namespace all PHP code outside of theme templates. This means any PHP file that isn’t part of the WordPress Template Hierarchy should be organized within a namespace or pseudo namespace so its contents don’t conflict with other, similarly-named classes and functions (“namespace collisions”).
Generally, this means including a PHP namespace identifier at the top of included files:
1
<?php
2
namespace Client\Package;
3
4
function do_something() {
5
// ...
6
}
Copied!
A namespace identifier consists of a top-level namespace, which is usually a client’s name. e.g. Client;
Additional levels of the namespace are defined at discretion of the project’s lead engineers. Around the time of a project’s kickoff, they agree on a strategy for namespacing the project’s code. For example, the client’s name may be followed with the name of a particular site or high-level project we’re building (Client\Package;).
When we work on more than one project for a client and we build common plugins shared between sites, “Common” might be used in place of the project name to signal this code’s relationship to the rest of the codebase.
The engineering leads document this strategy so it can be shared with engineers brought onto the project throughout its lifecycle.
use declarations should be used for classes outside a file’s namespace. By declaring the full namespace of a class we want to use once at the top of the file, we can refer to it by just its class name, making code easier to read. It also documents a file’s dependencies for future developers.
1
<?php
2
/**
3
* Example of a 'use' declaration.
4
*/
5
namespace Client\Package;
6
use Client\Package\Common\TwitterAPI;
7
8
function do_something() {
9
// Hard to read.
10
$twitter_api = new Client\Package\Common\TwitterAPI();
11
// Preferred.
12
$twitter_api = new TwitterAPI();
13
}
Copied!
If the code is for general release to the WordPress.org theme or plugin repositories, the minimum PHP compatibility of WordPress itself must be met, which is currently 5.6+. An alternative to PHP namespaces would be to instead create a class that would be used to wrap static functions to serve as a pseudo namespace:
1
<?php
2
/**
3
* Namespaced class name example.
4
*/
5
class Client_Utilities_API {
6
public static function do_something() {
7
// ...
8
}
9
}
Copied!
The similar structure of the namespace and the static class will allow for simple onboarding to either style of project (and a quick upgrade to PHP namespaces if the legacy project see value in doing so).
Anything declared in the global namespace, including a namespace itself, should be written in such a way as to ensure uniqueness. A namespace like XWP is (most likely) unique; theme is not. A simple way to ensure uniqueness is to prefix a declaration with a unique prefix.

Object Design

Firstly, if a function is not specific to an object, it should be included in a functional namespace as referenced above.
Objects should be well-defined, atomic, and fully documented in the leading docblock for the file. Every method and property within the object must themselves be fully documented, and relate to the object itself.
1
<?php
2
/**
3
* Video.
4
*
5
* This is a video object that wraps both traditional WordPress posts
6
* and various YouTube meta information APIs hidden beneath them.
7
*
8
* @package ClientTheme
9
* @subpackage Content
10
*/
11
class Prefix_Video {
12
13
/**
14
* WordPress post object used for data storage.
15
*
16
* @access protected
17
* @var WP_Post
18
*/
19
protected $_post;
20
21
/**
22
* Default video constructor.
23
*
24
* @access public
25
*
26
* @see get_post()
27
* @throws Exception Throws an exception if the data passed is not a post or post ID.
28
*
29
* @param int|WP_Post $post Post ID or WP_Post object.
30
*/
31
public function __construct( $post = null ) {
32
if ( null === $post ) {
33
throw new Exception( 'Invalid post supplied' );
34
}
35
36
$this->_post = get_post( $post );
37
}
38
}
Copied!

Visibility

In terms of Object-Oriented Programming (OOP), public properties and methods should obviously be public. Anything intended to be private should actually be specified as protected. There should be no private fields or properties without well-documented and agreed-upon rationale.

Structure and Patterns

    You can use the wp-foo-bar plugin scaffold for initializing new plugins.
    Use the dependency injection pattern in your plugin's object-oriented architecture.
    Put all plugin PHP code inside of a php directory. All plugin code should be encapsulated in classes, and so each file in this directory should follow the pattern class-foo-bar.php(for class Foo_Bar) according to WordPress naming conventions.
    Singletons are not advised. There is little justification for this pattern in practice and they cause more maintainability problems than they fix.
    Class inheritance should be used where possible to produce DRY code and share previously-developed components throughout the application.
    Global variables should be avoided. If objects need to be passed throughout the theme or plugin, those objects should either be passed as parameters or referenced through an object factory.
    Hidden dependencies (API functions, super-globals, etc) should be documented in the docblock of every function/method or property.
    Avoid registering hooks in the __construct method. Doing so tightly couples the hooks to the instantiation of the class and is less flexible than registering the hooks via a separate method. Unit testing becomes much more difficult as well.

Decouple Plugin and Theme using add_theme_support

The implementation of a custom plugin should be decoupled from its use in a Theme. Disabling the plugin should not result in any errors in the Theme code. Similarly switching the Theme should not result in any errors in the Plugin code.
The best way to implement this is with the use of add_theme_support and current_theme_supports.
Consider a plugin that adds a custom javascript file to the page post type. The Theme should register support for this feature using add_theme_support,
1
<?php
2
add_theme_support( 'custom-js-feature' );
Copied!
And the plugin should check that the current theme has indicated support for this feature before adding the script to the page, using current_theme_supports,
1
<?php
2
if ( current_theme_supports( 'custom-js-feature' ) ) {
3
// ok to add custom js
4
}
Copied!

Asset Versioning

It’s always a good idea to keep assets versioned, to make cache busting a simpler process when deploying new code. Fortunately, wp_register_script and wp_register_style provide a built-in API that allows engineers to declare an asset version, which is then appended to the file name as a query string when the asset is loaded.
It is recommended that engineers use a constant to define their theme or plugin version, then reference that constant when using registering scripts or styles. For example:
1
<?php
2
define( 'THEME_VERSION', '0.1.0' );
3
4
wp_register_script( 'custom-script', get_template_directory_uri() . '/js/asset.js', array(), THEME_VERSION );
Copied!
Remember to increment the version in the defined constant prior to deployment.

Security

Security in the context of web development is a huge topic. This section only addresses some of the things we can do at the server-side code level.

Input Validation and Sanitization

To validate is to ensure the data you’ve requested of the user matches what they’ve submitted. Sanitization is a broader approach ensuring data conforms to certain standards such as an integer or HTML-less text. The difference between validating and sanitizing data can be subtle at times and context-dependent.
Validation is always preferred to sanitization. Any non-static data that is stored in the database must be validated or sanitized. Not doing so can result in creating potential security vulnerabilities.
Sometimes it can be confusing as to which is the most appropriate for a given situation. Other times, it’s even appropriate to write our own sanitization and validation methods.
Here’s an example of validating an integer stored in post meta:
1
<?php
2
if ( isset( $_POST['user_id'] ) && absint( $_POST['user_id'] ) === $_POST['user_id'] ) {
3
update_post_meta( $post_id, 'key', absint( $_POST['user_id'] ) );
4
}
5
?>
Copied!
$_POST['user_id'] is validated using absint() which ensures an integer >= 0. Without validation (or sanitization), $_POST['user_id'] could be used maliciously to inject harmful code or data into the database.
Here is an example of sanitizing a text field value that will be stored in the database:
1
<?php
2
if ( isset( $_POST['special_heading'] ) ) {
3
update_option( 'option_key', sanitize_text_field( $_POST['special_heading'] ) );
4
}
5
?>
Copied!
Since update_option() is storing in the database, the value must be sanitized (or validated). The example uses the sanitize_text_field() function, which is appropriate for sanitizing general text fields.

Raw SQL Preparation and Sanitization

There are times when dealing directly with SQL can’t be avoided. WordPress provides us with $wpdb.
Special care must be taken to ensure queries are properly prepared and sanitized:
1
<?php
2
global $wpdb;
3
4
$wpdb->get_results( $wpdb->prepare( "SELECT id, name FROM $wpdb->posts WHERE ID='%d'", absint( $post_id ) ) );
5
?>
Copied!
$wpdb->prepare() behaves like sprintf() and essentially calls mysqli_real_escape_string() on each argument. mysqli_real_escape_string() escapes characters like ' and " which prevents many SQL injection attacks.
By using %d in sprintf(), we are ensuring the argument is forced to be an integer. You might be wondering why absint() was used since it seems redundant. It’s better to over sanitize than to miss something accidentally.
Here is another example:
1
<?php
2
global $wpdb;
3
4
$wpdb->insert( $wpdb->posts, array( 'post_content' => wp_kses_post( $post_content ), array( '%s' ) ) );
5
?>
Copied!
$wpdb->insert() creates a new row in the database. $post_content is being passed into the post_content column. The third argument lets us specify a format for our values sprintf() style. Forcing the value to be a string using the %s specifier prevents many SQL injection attacks. However, wp_kses_post() still needs to be called on $post_content as someone could inject harmful JavaScript otherwise.

Escape or Validate Output

To escape is to ensure data conforms to specific standards before being passed off. Validation, again, ensures that data matches what is to be expected in a much stricter way. Any non-static data outputted to the browser must be escaped or validated.
WordPress has a number of core functions that can be leveraged for escaping. We follow the philosophy of late escaping. This means we escape things just before output in order to reduce missed escaping and improve code readability.
Here are some simple examples of late-escaped output:
1
<div>
2
<?php echo esc_html( get_post_meta( $post_id, 'key', true ) ); ?>
3
</div>
Copied!
esc_html() ensures output does not contain any HTML thus preventing JavaScript injection and layout breaks.
Here is another example:
1
<a href="mailto:<?php echo sanitize_email( get_post_meta( $post_id, 'key', true ) ); ?>">Email me</a>
Copied!
sanitize_email() ensures output is a valid email address. This is an example of validating our data. A broader escaping function like esc_attr() could have been used, but instead sanitize_email() was used to validate.
Here is another example:
1
<input type="text" onfocus="if( this.value == '<?php echo esc_js( $fields['input_text'] ); ?>' ) { this.value = ''; }" name="name">
Copied!
esc_js() ensures that whatever is returned is safe to be printed within a JavaScript string. This function is intended to be used for inline JS, inside a tag attribute (onfocus=”…”, for example).
We should not be writing JavaScript inside tag attributes anymore, this means that esc_js() should never really be used. To escape strings for JS another function should be used instead, called wp_json_encode().
Here is another example:
1
<script>
2
if ( 0 <= document.cookie.indexOf( 'cookie_key' ) ) {
3
document.getElementById( 'test' ).getAttribute( 'href' ) = <?php echo wp_json_encode( get_post_meta( $post_id, 'key', true ) ); ?>;
4
}
5
</script>
Copied!
wp_json_encode() ensures that whatever is returned is safe to be printed in your JavaScript code. It returns a JSON encoded string.
Note that wp_json_encode() includes the string-delimiting quotes for you.
Sometimes you need to escape data that is meant to serve as an attribute. For that, you can use esc_attr() to ensure output only contains characters appropriate for an attribute:
1
<div class="<?php echo esc_attr( get_post_meta( $post_id, 'key', true ) ); ?>"></div>
Copied!
If you need to escape such that HTML is permitted (but not harmful JavaScript), the wp_kses_* functions can be used:
1
<div>
2
<?php echo wp_kses_post( get_post_meta( $post_id, 'meta_key', true ) ); ?>
3
</div>
Copied!
wp_kses_* functions should be used sparingly as they have bad performance due to a large number of regular expression matching attempts. If you find yourself using wp_kses_*, it’s worth evaluating what you are doing as a whole.
Are you providing a meta box for users to enter arbitrary HTML? Perhaps you can generate the HTML programmatically and provide the user with a few options to customize.
If you do have to use wp_kses_* on the frontend, output should be cached for as long as possible.
Translated text also often needs to be escaped on output.
Here’s an example:
1
<div>
2
<?php esc_html_e( 'An example localized string.', 'my-domain' ) ?>
3
</div>
Copied!
Instead of using the generic __() function, something like esc_html__() might be more appropriate. Instead of using the generic _e() function, esc_html_e() would instead be used.
There are many escaping situations not covered in this section. Everyone should explore the WordPress codex article on escaping output to learn more.

Nonces

In programming, a nonce, or number used only once, is a tool used to prevent CSRF or cross-site request forgery.
The purpose of a nonce is to make each request unique so an action cannot be replayed.
WordPress’ implementation of nonces are not strictly numbers used once, though they serve an equal purpose.
The literal WordPress definition of nonces is “A cryptographic token tied to a specific action, user, and window of time.”. This means that while the number is not a true nonce, the resulting number is specifically tied to the action, user, and window of time for which it was generated.
Let’s say you want to trash a post with ID 1. To do that, you might visit this URL: https://example.com/wp-admin/post.php?post=1&action=trash
Since you are authenticated and authorized, an attacker could trick you into visiting a URL like this: https://example.com/wp-admin/post.php?post=2&action=trash
For this reason, the trash action requires a valid WordPress nonce.
After visiting https://example.com/wp-admin/post.php?post=1&action=trash&_wpnonce=b192fc4204, the same nonce will not be valid in https://example.com/wp-admin/post.php?post=2&action=trash&_wpnonce=b192fc4204.
Update and delete actions (like trashing a post) should require a valid nonce.
Here is some example code for creating a nonce:
1
<form method="post" action="">
2
<?php wp_nonce_field( 'my_action_name' ); ?>
3
...
4
</form>
Copied!
When the form request is processed, the nonce must be verified:
1
<?php
2
// Verify the nonce to continue.
3
if ( isset( $_POST['_wpnonce'] ) && wp_verify_nonce( $_POST['_wpnonce'], 'my_action_name' ) ) {
4
// Nonce is valid!
5
}
6
?>
Copied!

Internationalization

All text strings in a project should be internationalized using core localization functions. Even if the project does not currently dictate a need for translatable strings, this practice ensures translation-readiness should a future need arise.
WordPress provides a myriad of localization functionality. Engineers should familiarize themselves with features such as pluralization and disambiguation so translations are flexible and translators have the information they need to work accurately.
Samuel Wood (Otto) put together a guide to WordPress internationalization best practices, and engineers should take time to familiarize themselves with its guidance: Internationalization: You’re probably doing it wrong
It’s important to note that the strings passed to translation functions should always be literal strings, never variables or named constants, and code shouldn’t use string interpolation to inject values into either string. Most tools used to create translations rely on GNU gettext scanning source code for translation functions. PHP code won’t be interpreted, only scanned like it was a block of plain text and stored similarly. If WordPress’s translation APIs can’t find an exact match for a string inside the translation files, it won’t be able to translate the string. Instead, use printf() formatting codes inside the string to be translated and pass the translated version of that string to sprintf() to fill in the values.
For example:
1
<?php
2
// This will confuse translation software
3
$string = __( "$number minutes left", 'plugin-domain' );
4
// So will this
5
define( 'MINUTES_LEFT', '%d minutes left' );
6
$string = __( MINUTES_LEFT, 'plugin-domain' );
7
// Correct way to do a simple translation
8
$string = sprintf( __( '%d minutes left', 'plugin-domain' ), $number );
9
// A more complex translation using _n() for plurals
10
$string = sprintf( _n( '%d minute left', '%d minutes left', $number, 'plugin-domain' ), $number );
11
?>
Copied!
Localizing a project differs from the core approach in two distinct ways:
    A unique text domain should be used with all localization functions
    Internationalized output should always be escaped

Text Domains

Each project should leverage a unique text domain for its strings. Text domains should be lowercase, alphanumeric, and use hyphens to separate multiple words: xwp-project-name.
Like the translated strings they accompany, text domains should never be stored in a variable or named constant when used with core localization functions, as this practice can often produce unexpected results. Translation tools won’t interpret PHP code, only scan it like it was plain text. They won’t be able to assign the text domain correctly if it’s not there in plain text.
1
<?php
2
// Always this
3
$string = __( 'Hello World', 'plugin-domain' );
4
// Never this
5
$string = __( 'Hello World', $plugin_domain );
6
// Or this
7
define( 'PLUGIN_DOMAIN', 'plugin-domain' );
8
$string = __( 'Hello World', PLUGIN_DOMAIN );
9
?>
Copied!
If the code is for release as a plugin or theme in the WordPress.org repositories, the text domain must match the directory slug for the project in order to ensure compatibility with the WordPress language pack delivery system. The text domain should be defined in the “Text Domain” header in the plugin or stylesheet headers, respectively, so the community can use GlotPress to provide new translations.

Escaping Strings

Most of WordPress’s translation functions don’t escape output by default. So, it’s important to escape the translated strings like any other content.
To make this easier, the WordPress API includes functions that translate and escape in a single step. Engineers are encouraged to use these functions to simplify their code:

For use in HTML

    1.
    esc_html__: Returns a translated and escaped string
    2.
    esc_html_e: Echoes a translated and escaped string
    3.
    esc_html_x: Returns a translated and escaped string, passing a context to the translation function

For use in attributes

    1.
    esc_attr__: Returns a translated and escaped string
    2.
    esc_attr_e: Echoes a translated and escaped string
    3.
    esc_attr_x: Returns a translated and escaped string, passing a context to the translation function

Code Style & Documentation

We follow the official WordPress coding and documentation standards. The WordPress Coding Standards for PHP_CodeSniffer will find many common violations and flag risky code for manual review. Note that there are pre-defined standards. You will probably want to customize the PHPCS ruleset for your project: wp-dev-lib will look for a phpcs.ruleset.xml to use for the pre-commit hook and Travis CI build. Please also be aware of the PHP_CodeSniffer-bundled tool phpcbf (PHP Code Beautifier and Fixer) which also automatically fixes many WordPress Coding Standard violations.
That said, at XWP we highly value verbose commenting/documentation throughout any/all code, with an emphasis on docblock long descriptions which state ‘why’ the code is there and ‘what’ exactly the code does in human-readable prose. As a general rule of thumb; a manager should be able to grok your code by simply reading the docblock and inline comments.
Example:
1
<?php
2
/**
3
* Hook into WordPress to mark specific post meta keys as protected
4
*
5
* Post meta can be either public or protected. Any post meta which holds
6
* **internal or read only** data should be protected via a prefixed underscore on
7
* the meta key (e.g. _my_post_meta) or by indicating it's protected via the
8
* is_protected_meta filter.
9
*
10
* Note, a meta field that is intended to be a viewable component of the post
11
* (Examples: event date, or employee title) should **not** be protected.
12
*/
13
add_filter( 'is_protected_meta', 'protect_post_meta', 10, 2 );
14
15
/**
16
* Protect non-public meta keys
17
*
18
* Flag some post meta keys as private so they're not exposed to the public
19
* via the Custom Fields meta box or the JSON REST API.
20
*
21
* @internal Called via is_protected_meta filter.
22
* @param bool $protected Whether the key is protected. Default is false.
23
* @param string $current_meta_key The meta key being referenced.
24
* @return bool $protected The (possibly) modified $protected variable
25
*/
26
function protect_post_meta( $protected, $current_meta_key ) {
27
28
// Assemble an array of post meta keys to be protected
29
$meta_keys_to_be_protected = array(
30
'my_meta_key',
31
'my_other_meta_key',
32
'and_another_meta_key',
33
);
34
35
// Set the protected var to true when the current meta key matches
36
// one of the meta keys in our array of keys to be protected
37
if ( in_array( $current_meta_key, $meta_keys_to_be_protected ) ) {
38
$protected = true;
39
}
40
41
// Return the (possibly modified) $protected variable.
42
return $protected;
43
}
44
?>
Copied!

Unit and Integration Testing

Unit testing is the automated testing of units of source code against certain assertions. The goal of unit testing is to write test cases with assertions that test if a unit of code is truly working as intended. If an assertion fails, a potential issue is exposed, and code needs to be revised.
By definition, unit tests do not have dependencies on outside systems; in other words, only your code (a single unit of code) is being tested. Integration testing works similarly to unit tests but assumptions are tested against systems of code, moving parts, or an entire application. The phrases unit testing and integration testing are often misused to reference one another especially in the context of WordPress.
We generally employ unit and integration tests only when building applications that are meant to be distributed. Building tests for client themes doesn’t usually offer a huge amount of value (there are of course exceptions to this). When we do write tests, we use PHPUnit which is the WordPress standard library.
For fundamentals and a general overview of unit tests at XWP, see the workflows section on unit testing; also see specifics on PHP unit testing and JS unit testing.
The wp-dev-lib project includes a PHPUnit bootstrap and phpunit.xml for plugins which facilitates writing tests in pretty much any type of Vagrant or Docker environment running WordPress. Read more about wp-dev-lib and automated testing.
On the topic of test coverage: since a plugin's PHP code should be comprised of classes, a convenient way to organize PHPUnit tests is to have one test case class per plugin class, and one (at least) test method per plugin class method. Use the plugin class and method in the naming for the unit test class and test method, also including the a PhpDoc @see tag to explicitly list out the method that is being tested. For instance, given a plugin Foo that has a class Bar in a file php/class-bar.php:
1
<?php
2
namespace Foo;
3
4
class Bar {
5
/**
6
* @return int
7
*/
8
public function baz() {
9
/* ... */
10
return $quux;
11
}
12
}
Copied!
This can have a corresponding unit test case in tests/test-bar.php:
1
<?php
2
namespace Foo;
3
4
class Test_Bar extends \WP_UnitTestCase {
5
/**
6
* @see Bar::baz()
7
*/
8
function test_baz() {
9
$foo = new Foo();
10
$this->assertInternalType( 'int', $foo->baz() );
11
// ...
12
}
13
}
Copied!
In addition to organizing unit tests with this class/method correspondence, PHPUnit itself has powerful code coverage analytics that it can generate in a beautiful report. The wp-dev-lib phpunit.xml also includes a filterconfiguration for restricting the list of PHP files to just those in the plugin when running the code coverage report which can be generated via:
1
phpunit --coverage-html code-coverage-report/
Copied!
See also JS Unit Testing.

Libraries and Frameworks

Generally, we do not use PHP frameworks or libraries that do not live within WordPress for general theme and plugin development. WordPress APIs provide us with 99 percent of the functionality we need from database management to sending emails. There are frameworks and libraries we use for themes and plugins that are being distributed or open-sourced to the public such as PHPUnit.

Avoid Heredoc and Nowdoc

PHP’s doc syntaxex construct large strings of HTML within code, without the hassle of concatenating a bunch of one-liners. They tend to be easier to read, and are easier for inexperienced front-end developers to edit without accidentally breaking PHP code.
1
$y = <<<JOKE
2
I told my doctor
3
"it hurts when I move my arm like this".
4
He said, "<em>then stop moving it like that!</em>"
5
JOKE;
Copied!
However, heredoc/nowdoc make it impossible to practice late escaping:
1
// Early escaping
2
$a = esc_attr( $my_class_name );
3
4
// Something naughty could happen to the string after early escaping
5
$a .= 'something naughty';
6
7
// XWP & VIP prefer to escape right at the point of output, which would be here
8
echo <<<HTML
9
<div class="test {$a}">test</div>
10
HTML;
Copied!
As convenient as they are, engineers should avoid heredoc/nowdoc syntax and use traditional string concatenation & echoing instead. The HTML isn’t as easy to read. But, we can be sure escaping happens right at the point of output, regardless of what happened to a variable beforehand.
1
// Something naughty could happen to the string...
2
$my_class_name .= 'something naughty';
3
4
// But it doesn't matter if we're late escaping
5
echo '<div class="test ' . esc_attr( $my_class_name ) . '">test</div>';
Copied!
Even better, use WordPress’ get_template_part() function as a basic template engine. Make your template file consist mostly of HTML, with <?php ?> tags just where you need to escape and output. The resulting file will be as readable as a heredoc/nowdoc block, but can still perform late escaping within the template itself.

Avoid Sessions

Sessions add extra complexity to web sites and extra burden on hosting setups. Avoid using sessions to store individual users’ preferences or other data. WordPress VIP explicitly forbids sessions in their code reviews.
Instead of sessions, use cookies or client-side storage APIs if possible. In addition to keeping this data off the web servers, they empower site visitors to view and delete the data tied to their activity. Systems Engineers can configure full-page caches to ignore custom cookies so they don’t interfere with caching.
If sessions must be used, create them conservatively. Don’t create sessions for every visitor. Limit sessions to the smallest group that needs them: logged-in editors and admins, or visitors using a particular feature.
Sessions should never be stored in the database. This introduces extra data into a storage system that’s not meant for that volume. Database session libraries also rely on PHP code which can’t match the performance of PHP’s native session handlers. PHP extensions for Memcache and Redis allow sessions to be stored in these in-memory datastores and are a good solution for sessions when multiple webservers are present. For example, like when WordPress is deployed to App Engine on Google Cloud Platform where containers are spread across many regions or zones and you need to keep track of logged in users as different containers serve requests.
Last modified 2yr ago