Approaching Modern WordPress Development: Part 2

WordPress is the world’s most popular website technology for a good reason. However, its core legacy code can be messy, impacting third-party developers. While some developers might see this as a reason to take shortcuts in their WordPress PHP code, this strategy often leads to higher expenses in the long term, except for very minor modifications.

Part 1 of our WordPress programming series explored project and workflow tools and front-end development. Now, in Part 2, we tackle PHP, the language behind WordPress, focusing on best practices for back-end code. Think of this as a slightly more advanced PHP/WordPress back-end guide, assuming you have some experience with WordPress back-end development and customization.

Let’s explore ten highly recommended WordPress and PHP development practices to maximize your time investment.

Best Practice #1: Embrace “Separation of Concerns”

Separation of concerns dictates that WordPress PHP code sections with different functions or purposes should be separate, organized into distinct sections or modules. These modules interact by passing data through a defined interface, which outlines the input parameters a module accepts and its output. A related principle is the single responsibility principle: each code module (or function) should have only one responsibility.

These principles aim to create modular code that is maintainable, extensible, and reusable.

Let’s illustrate with an example of WordPress PHP code (from WordPress core) where everything is intertwined, often called “spaghetti code” due to its complexity. While redacted for brevity, the following excerpt retains its original original style and formatting:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
$id = isset( $_REQUEST['id'] ) ? intval( $_REQUEST['id'] ) : 0;
<table class="form-table">
    <?php
    $blog_prefix = $wpdb->get_blog_prefix( $id );
    $sql         = "SELECT * FROM {$blog_prefix}options
        WHERE option_name NOT LIKE %s
        AND option_name NOT LIKE %s";
    $query       = $wpdb->prepare(
        $sql,
        $wpdb->esc_like( '_' ) . '%',
        '%' . $wpdb->esc_like( 'user_roles' )
    );
    $options     = $wpdb->get_results( $query );
    foreach ( $options as $option ) {
        if ( strpos( $option->option_value, "\n" ) === false ) {
            ?>
            <tr class="form-field">
                <th scope="row"><label for="<?php echo esc_attr( $option->option_name ); ?>"><?php echo esc_html( ucwords( str_replace( '_', ' ', $option->option_name ) ) ); ?></label></th>
                <?php if ( $is_main_site && in_array( $option->option_name, array( 'siteurl', 'home' ) ) ) { ?>
                <td><code><?php echo esc_html( $option->option_value ); ?></code></td>
                <?php } else { ?>
                <td><input class="<?php echo $class; ?>" name="option[<?php echo esc_attr( $option->option_name ); ?>]" type="text" id="<?php echo esc_attr( $option->option_name ); ?>" value="<?php echo esc_attr( $option->option_value ); ?>" size="40" <?php disabled( $disabled ); ?> /></td>
                <?php } ?>
            </tr>
            <?php
            }
    } // End foreach
</table>

This code is incredibly difficult to understand, even with the redundant comment End foreach. We see database querying, query result processing, processing within HTML (including a nested if/else statement), output escaping, and HTML templating all mixed together. Another issue is the $id parameter taken directly from the global $_REQUEST instead of being passed as a function parameter.

This code demonstrates why WordPress core has remained largely unchanged for years—refactoring such code while preserving existing behavior is a massive task.

So, how can we improve this? Remember, there is no single correct approach. Our goal is to create maintainable and modular WordPress custom PHP code. Let’s break down the code into modules:

  • SQL queries should reside in a dedicated module, ideally utilizing WordPress’s abstracted WP_Query class.
  • All HTML should be moved to a template, which we’ll cover later in PHP templating.
  • The remaining PHP code should be encapsulated in a function (or multiple functions for lengthy or complex code). Parameters like $id should be passed as function arguments.

Here’s a simplified rewrite of the example:

1
2
3
4
5
6
7
function betterSiteSettings($args)
{
    $data = WP_Settings_Query($args);
    // process $data here
    $context = array_merge([], $data_processed, $other_data);
    return Template::render('template.name', $context);
}

Best Practice #2: Minimize Global Variable Usage

WordPress relies heavily on global variables, which can negatively impact code maintainability and application state reliability. Why? Because any PHP code, including installed WordPress plugins, can read and modify these global variables, jeopardizing data integrity. Even understanding the role of global variables within something like the Loop can be challenging.

Let’s examine a practical example from WooCommerce—the familiar WordPress Loop:

1
2
3
<?php while ( have_posts() ) : the_post(); ?>
<?php wc_get_template_part( 'content', 'single-product' ); ?>
<?php endwhile; // end of the loop. ?>

This snippet renders a product template. However, wc_get_template_part lacks parameters, so how does it determine the product to display? Looking at the template, we see global $product;—this is where the current product object is stored.

Now, envision a catalog page with product search and filtering. We aim to show a “product details” popup without leaving the page. The front-end script uses AJAX to fetch the product template. Since wc_get_template_part('content', 'single-product') doesn’t accept parameters, we need to manipulate global variables for this to work.

More complex scenarios might involve multiple templates, hooks within those templates, and third-party plugins adding callbacks to these hooks. This can quickly spiral out of control, as we cannot guarantee the global state these callbacks rely on. Third-party plugins have the freedom to modify any global variable in their callbacks, forcing us to fight the system and debug issues arising from unreliable global state.

Wouldn’t it be more efficient to pass the product ID as a parameter? This would allow us to reuse the template without the risk of disrupting global variables used by WordPress.

Best Practice #3: Employ Object-oriented Programming (OOP)

Modularity naturally leads to objects and object-oriented programming. At its core, OOP is a way to structure code by grouping functions (called class methods in OOP) and variables (properties) within classes. The WordPress Plugin Handbook recommends using OOP for organizing your WordPress custom PHP code.

A key OOP principle in WordPress programming is controlling access to methods and properties. By designating them as private or protected in PHP, we ensure only other class methods can access and modify them. This concept, known as encapsulation, safeguards data within the class, allowing modifications only through specific class methods.

Compared to using global variables modifiable from anywhere in the codebase, this approach significantly simplifies debugging and maintenance. Consider the global WordPress post variable, accessible and relied upon by numerous functions throughout the codebase. What if we could restrict modifications to WordPress core functions while still allowing read access for others? Encapsulating the global post variable within a class and building an interface around it would achieve this.

This is a basic overview of OOP and its applications in modern WordPress development. For a deeper dive, Carl Alexander’s e-book, Discover object-oriented programming using WordPress, offers a comprehensive and insightful exploration of OOP in WordPress.

Remember that OOP is not a magic solution—poor code can be written using OOP just as easily as with other programming paradigms.


Now, let’s delve into specific advice on using PHP for WordPress development.

PHP Best Practice #1: Target PHP 7.0+

Leveraging modern PHP features requires, unsurprisingly, a modern PHP version. Supporting PHP versions older than 7.0 is no longer necessary, especially since WordPress core itself will mandate PHP 7.0 starting with the end of 2019.

However, it’s always a good practice to verify your minimum PHP version to prevent compatibility issues and the dreaded “white screen of death”. The following snippet demonstrates how to declare a minimum PHP version using a plugin header and include a guard condition in your code:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
<?php
/**
 * Plugin Name: My Awesome Plugin
 * Requires PHP: 7.0
 */

// bails if PHP version is lower than required
if (version_compare(PHP_VERSION, '7.0.0', '<')) {
    // add admin notice here
    return;
}

// the rest of the actual plugin here

PHP Best Practice #2: Adhere to PHP Industry Standards (PSR-2 Coding Style Guide)

PSRs, or PHP Standard Recommendations, are published by the PHP Framework Interop Group and serve as de facto industry standards widely adopted by the PHP community. PSR-2 specifically addresses coding style. Popular PHP frameworks like Symfony and Laravel adhere to PSR-2.

Why choose PSR-2 over the WordPress coding standard? Primarily because WordPress standards are outdated and don’t utilize newer language features. This is understandable considering WordPress core’s need to maintain backward compatibility with older PHP versions, including 5.2 until recently, which is incompatible with PSR-2.

Unless you’re contributing directly to WordPress core, there’s no strict requirement to follow WordPress coding standards. Submitting a plugin adhering to PSR-2 to the WordPress plugin directory is perfectly acceptable. In fact, there are compelling very good arguments for doing so.

PHP Best Practice #3: Utilize a PHP Template Engine

PHP, despite its origins, is not a template engine. While it started as one, it has evolved into a full-fledged programming language. Using dedicated template engines offers significant advantages.

Two popular options are Twig and Blade, employed by Symfony and Laravel, respectively. This article will use Twig as an example, but Blade offers comparable features and functionality. I encourage you to explore both and determine which best suits your needs.

The following example contrasts a PHP template with its Twig equivalent. Notice the verbose syntax for displaying and escaping output in the PHP version:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
foreach ( $options as $option ) {
    ?>
    <tr class="form-field">
        <th scope="row">
            <label for="<?php echo esc_attr( $option->option_name ); ?>">
                <?php echo esc_html( strtolower( $option->option_name ) ); ?>
            </label>
        </th>
    </tr>
    <?php
} // End foreach

In Twig, the same code becomes more concise and readable:

1
2
3
4
5
6
7
8
9
{% for option in options %}
    <tr class="form-field">
        <th scope="row">
            <label for="{{ option.option_name }}">
                {{ option.option_name }}
            </label>
        </th>
    </tr>
{% endfor %}

Twig’s advantages include:

  • Clean and concise syntax
  • Automatic output escaping
  • Template extension through inheritance and blocks

Performance-wise, Twig compiles into PHP templates with minimal overhead. Its limited subset of PHP language constructs, focused solely on templating, encourages developers to separate business logic from templates, reinforcing separation of concerns.

There’s even a Twig implementation for WordPress called Timber, a great starting point for building better templates. The Timber starter theme exemplifies how to structure themes using OOP principles.

PHP Best Practice #4: Embrace Composer

Composer is an indispensable dependency manager for PHP. This tool simplifies project management by allowing you to declare required libraries. Composer handles downloading, installing, and updating these libraries, eliminating the need for manual require statements for each library. You simply include Composer’s autoload file, vendor/autoload.php.

However, WordPress plugins and themes often have limited use for third-party libraries. This is partly because WordPress has a comprehensive API that covers most needs and partly due to potential version conflicts. For instance, if two plugins depend on different versions of the same library, the plugin loaded first dictates the version used, potentially causing conflicts and the dreaded white screen of death.

To mitigate conflicts, dependency management should be implemented at the application level, meaning the entire WordPress site. This is precisely what Roots (specifically Bedrock) does. At the package level (plugin or theme), Composer’s use of third-party libraries can create conflicts, a problem known as known issue. The current workaround involves renaming the external library’s namespaces to ensure uniqueness and then that’s not a trivial task.

Despite these challenges, Composer still offers a valuable feature: autoloading your own classes. But before we explore autoloading, let’s discuss PHP namespaces.

PHP Best Practice #5: Utilize Namespaces

As a legacy project, WordPress core operates in the global namespace—essentially, no namespace at all. Any globally declared classes or functions (those not within another class or function) are visible throughout the entire codebase. Their names must be unique, not just within your code, but across all active and potentially future plugins and themes.

Name collisions (e.g., declaring a function with an existing name) usually lead to fatal errors and the dreaded white screen of death. The WordPress Codex recommends prefixing functions and classes with a unique identifier. Instead of a simple class name like Order, we end up with something like Akrte_Awesome_Plugin_Order, where “Akrte” serves as the unique prefix.

Namespaces offer a more organized approach. They act as groups (or folders, using a filesystem analogy) that prevent name collisions. Like nested folders, you can have complex namespaces separated by backslashes. (PHP namespaces use a backslash, for instance.)

These namespace components are called sub-namespaces. Our example class, Akrte_Awesome_Plugin_Order, becomes Akrte\Awesome_Plugin\Order using namespaces. Here, Akrte and Awesome_Plugin are namespace parts (sub-namespaces), while Order is the class name. By adding a use statement, you can refer to the class using only its name, resulting in cleaner code:

1
2
3
use Akrte\Awesome_Plugin\Order;

$a = new Order;

Naturally, namespaces need to be unique. Therefore, the first “root” sub-namespace, typically the vendor name, should be distinctive. For example, the WooCommerce class WC_REST_Order_Notes_V2_Controller could be rewritten using namespaces like this:

1
2
namespace WooCommerce\RestApi\V2\Controllers;
class OrderNotes {}

The latest WooCommerce codebase has adopted namespaces, as seen in the WooCommerce REST API version 4.

PHP Best Practice #6: Implement an Autoloader

Traditionally, PHP files are linked using require or include statements. However, as projects grow, the main plugin file can become cluttered with dozens of require statements. Autoloaders automate this process and include files only when needed.

An autoloader is essentially a function that requires a file containing a specific class or function when it’s first encountered in the code, eliminating the need for manual require statements.

This approach often results in noticeable performance improvements, as the autoloader only loads necessary modules for a particular request. Without it, your entire codebase is loaded even if only a small portion is actually used.

Autoloader functions need to locate classes and functions within files. The PHP-FIG standard, PSR-4, provides guidelines for this.

It suggests mapping a portion of the namespace (the prefix) to a base folder. Subsequent sub-namespaces correspond to folders within the base folder, and the class name matches the file name. For instance, a class named Akrte\AwesomePlugin\Models\Order would require the following folder structure:

1
2
3
4
5
/awesome-plugin
    awesome-plugin.php
    /includes
        /Models
            Order.php

The namespace prefix, Akrte\AwesomePlugin\, maps to the includes folder as defined in the autoloader configuration (discussed below). The Models sub-namespace has a corresponding Models folder, and the Order class is within Order.php.

Fortunately, you don’t have to create an autoloader function from scratch. Composer can generate one for you:

  1. Install Composer
  2. Create a composer.json file in your project’s root folder with the following lines:
1
2
3
4
5
6
7
8
9
{
    "name": "vendor-name/plugin-name",
    "require": {},
    "autoload": {
        "psr-4": {
            "Akrte\\AwesomePlugin\\": "includes/"
        }
    }
}
  1. Run composer install.
  2. Include vendor/autoload.php at the beginning of your main plugin PHP file:
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
    <?php
    /**
     * Plugin Name: My Awesome Plugin
     */

    defined('ABSPATH') || exit;

    require __DIR__ . '/vendor/autoload.php';

    // do stuff

Along with namespaces, the latest WooCommerce codebase utilizes a Composer autoloader.


Having covered these PHP design principles, let’s connect our PHP lessons back to WordPress back-end customization with one final recommendation.

WordPress Development Best Practice #4: Explore the Roots Stack

Roots](https://roots.io/) is the [most complete modern WordPress development workflow available. However, it’s not necessarily suitable for every WordPress project due to a few factors:

  • Roots is best adopted from the start. Refactoring an existing project to use Roots can be prohibitively expensive.
  • It’s an opinionated framework. This is beneficial if you agree with its approach but problematic if you don’t. You might have different preferences for structuring themes, for instance. Opinionated projects also demand time to learn their specific conventions.
  • Not everyone is familiar with it. Developers who inherit your Roots-based project might not understand its structure and wonder about the modified WordPress folders. It’s crucial to consider the maintainability of your code for fellow WordPress developers.

Always carefully weigh the pros and cons of any strongly opinionated project before committing to it.

Modern PHP and Software Principles: Building Robust WordPress Back-ends

The ever-evolving landscape of software development means there is no single “right” way to write code. While fundamental concepts like separation of concerns have existed for decades, their practical implementations remain subject to debate. Take CSS, for example. We began by inlining it using the style attribute in HTML. Then, separate CSS files emerged as the preferred approach for separation of concerns.

Fast forward to today, where modern JavaScript applications use components to achieve separation of concerns, and CSS-in-JS is gaining traction among front-end developers. This essentially brings us back to inlining CSS in HTML (though with added complexities).

Ultimately, best practices should aim to improve the developer experience:

Programs must be written for people to read, and only incidentally for machines to execute.

Abelson & Sussman, Structure and Interpretation of Computer Programs

Some practices outlined in this PHP WordPress tutorial are quick and easy to implement, such as using an autoloader—set it up once per project and reap the benefits. New software architecture ideas, on the other hand, require time, practice, and iteration to master. However, the payoff is well worth the effort. You’ll not only become more efficient but also derive more enjoyment from your work. And finding consistent satisfaction in the work you do for clients is perhaps the key to a sustainable career.

Licensed under CC BY-NC-SA 4.0