Update 28/6/21: We’ve now open-sourced our WordPress project setup, which is the base setup we use for all our WordPress projects. The main features are:

  • Docker for local development, including easy xdebug and blackfire support
  • Monorepo for easy distribution of common plugins/themes
  • Dictator for easy configuration management
  • Fixtures for easy and safe production like data
  • Composer setup for installing plugins
  • Dependency Injection (DI) container for managing objects
  • Feature flags for easy management of feature toggles

github.com/boxuk/wp-project-skeleton

What I’d like to do in this post is just share a little about how we develop WordPress sites, drawing on our experience working with the platform as well as a range of other technologies. We’re always learning from the excellent WordPress community, and I wanted to share our setup for anyone looking to learn more about the platform.

Local setup

Getting a local developer environment setup is something we do for all projects to provide developers with a quick feedback loop for the code that they write. Across all our projects we’ve gone on a journey throughout the years from custom-built Virtual Machines (VMs), Vagrant, Ansible, and more recently Docker. It made sense for us to therefore stick to what we use for all our other projects and use Docker. Due to the experience we have and the setups we have across various projects, this is a big win for us. Docker itself is pretty popular in the industry as a whole too and is often the recommended tool for local setup. There can be some pain points with performance on certain OSs but generally, we don’t suffer too much.

There are plenty of alternatives out there however, DeliciousBrains did a pretty good run-through on their blog earlier this year.

What’s in our Docker setup?

Typically for most of our WordPress sites, we’ll have the following containers:

  • App: our main app container running php-fpm and the app itself.
  • App xdebug: a version of our app with xdebug enabled, I’ll explain later why it’s in its own container.
  • Styleguide: php-fpm running the project’s pattern library/styleguide.
  • Database: MariaDB typically.
  • Server: Nginx typically.
  • Email testing tool: MailHog.
  • Caching system: Memcached.
  • JavaScript runner: NodeJS, to run things like Yarn, webpack, etc.
  • Profiler: Blackfire, for profiling our app easily.

How we run xdebug

We run xdebug in a separate container and route through that dynamically depending on a cookie value. This means we don’t have xdebug (which slows down performance considerably) running unless we really need it. We use a browser extension to easily toggle the cookie value and then this bit of magic in our Nginx container does the routing:

map $cookie_XDEBUG_SESSION $fastcgi_pass {
  default app;
  wordpress app_xdebug;
}

You can read more about this approach from my colleague Michal, who introduced this for us.

Monorepo

We’ve recently been experimenting with a monorepo setup. We have a base plugin we use for various utilities and things that we use for all projects, similarly, we have a boilerplate theme we use as a starting point for all our projects. These two things live in their own independent repositories as they are useful in their own right and don’t need to exist in the context of our WordPress base project. However, it becomes tiresome if you need to make changes to the base plugin or theme whilst working on a project and have to go through the following steps for each change:

  • Checkout plugin/theme
  • Copy across changes
  • Commit changes
  • Pull changes into the project

With a monorepo we can develop the base plugin and/or theme whilst working on the project and it will get automatically pushed to the separate repository on deployment. To help with this we use a tool called monorepo-builder which does all the heavy lifting.

We are only doing this for our base plugin and boilerplate theme at the moment but it would be useful if we start work on a generic plugin on a project and think it could be a standalone thing.

Dictator

From working across various enterprise CMSs over the years we knew that configuration was a big thing. Manual configuration of a single site, and even more so on a multi-site, can be very time-consuming and prone to human error. Therefore, we needed a method of rolling out configuration in an automated fashion, and across environments easily. This is where Dictator comes in. I wrote about Dictator earlier this year.

The long and short of it is, it allows us to easily roll out configuration based from a YAML file. For example:

state: site
settings:
  title: WordPress Site
  description: Just another WordPress site
  date_format: F j, Y
  time_format: g:i a
  active_theme: twentytwenty

The above configuration will set the title of the site to ‘WordPress Site’, set the theme to ‘twentywenty’, and so on. It supports a bunch of options for both single-site and multi-site setups and we’ve recently just released an extension that will allow you to configure WooCommerce using Dictator.

Fixtures

We aim to use fixtures across all our projects, across all platforms. There are heaps of reason to use fixtures and not rely on copies of databases shared around developers; here’s a summary of these reasons:

  • Reproducibility: easy to reproduce environments.
  • Consistency: data is the same for all developers.
  • Usability: ease of use. Ease of setup of environment.
  • Availability: easy access, no need to wait for a copy of a DB to be sent to you, or security sign off of production data.
  • Reusability: CI tests + local dev can all use the same data. Can also load onto testing environments such as staging.
  • Security: mitigates the risk of sensitive data existing on a developer’s machine.
  • Privacy: ensures no personal data exists on developers’ machines or staging environments. A must for GDPR.
  • Variety: easy variation of data to mimic edge case bugs that are to do with certain types of data.

We use wp-cli-fixtures for running our fixtures. We use Alice on other projects so wanted the same for WordPress.

Here’s an example of our fixtures file:

## Attachments
Hellonico\Fixtures\Entity\Attachment:
    default (template):
        post_title: <words(2, true)>
    pics{1..15} (extends default):
        file: <picsum(<uploadDir()>, 1200, 1200)>

## Terms
Hellonico\Fixtures\Entity\Term:
    category{1..10}:
        name (unique): <words(2, true)> # '(unique)' is required
        description: <sentence()>
        parent: '50%? <termId(childless=1)>' # 50% of created categories will have a top level parent category
    tag{1..100}:
        name (unique): <words(2, true)> # '(unique)' is required
        taxonomy: post_tag

Hellonico\Fixtures\Entity\Post:
    ## Pages
    home_page:
        post_title: Homepage
        post_type: page
        post_status: publish
        meta:
            _wp_page_template: front-page.php
    about_page:
        post_title: About
        post_type: page
        post_status: publish
        post_content: <paragraphs(5, true)>
    cookie_policy:
        post_title: Cookies Policy
        post_type: page
        post_status: publish
        post_content: <paragraphs(5, true)>
    privacy_policy:
        post_title: Privacy Policy
        post_status: publish
        post_type: page
        post_content: <paragraphs(5, true)>
    terms_and_conditions:
        post_title: Terms and Conditions
        post_status: publish
        post_type: page
        post_content: <paragraphs(5, true)>
    ## Posts
    default (template):
        post_title: <words(2, true)>
        post_date: <dateTimeThisDecade()>
        post_content: <paragraphs(5, true)>
        post_excerpt: <paragraphs(1, true)>
        meta:
            _thumbnail_id: '@pics*->ID'
    post{1..30} (extends default):
        post_category: '1x @category*->term_id'
        tax_input:
            post_tag: '5x @tag*->term_id'

## Menu
Hellonico\Fixtures\Entity\NavMenu:
    main_menu:
        name: Main Menu
        locations:
            - main_menu
    footer_menu:
        name: Footer
        locations:
            - footer_menu

## Menu Items
Hellonico\Fixtures\Entity\NavMenuItem:
    home:
        menu_item_object: '@home_page'
        menu_id: '@main_menu->term_id'
    about:
        menu_item_object: '@about_page'
        menu_id: '@main_menu->term_id'
        menu_item_parent_id: '@home->ID'
    about_footer:
        menu_item_object: '@about_page'
        menu_id: '@footer_menu->term_id'

Plugins

When working with WordPress, plugins can be a blessing and a curse. A blessing, because they allow you to expand the functionality and do so much more with WordPress than you might expect. A curse because it is an open marketplace and although the cream rises to the top, it can be hard to work out where the top ends.

How we install plugins

Although there is an official WordPress directory for plugins, not all plugins are necessarily listed there and in fact, there are several ways you can install plugins. Here’s a list of the various ways plugins can be installed:

We use composer across all our PHP projects (and similar dependency managers on other platforms) so we wanted to install all our plugins with composer. They are very much dependencies and therefore it made sense to use our standard dependency tool for installing plugins.

Our preferred setup is that a plugin is listed on wpackagist, which we add as a repository to our composer.json file. It mirrors all the plugins from the official plugin directory so most of the time a plugin is indeed listed there.

Our second preference would be to use regular packagist wherever possible. After this, things get a bit more complicated.

For plugins that don’t exist on wpackagist or packagist, we install them into a folder locally and copy them across to the plugins directory with composer using the path repository. This gives us the benefit of being able to continue to use composer for all our plugin installs. It does mean we hold a copy of the plugin in our git repository, but not in the plugins folder, which is important as it means it won’t get included for deployment.

wp-project/
├─ local-plugins/
│ ├─ some-plugin/
├─ wp-content/
│ ├─ plugins/
│ │ ├─ some-plugin/

Our composer repository setup for this would then look as thus:

"repositories": [
  {
    "options": {
      "symlink": false
    },
    "type": "path",
    "url": "./local-plugins/*"
  }
]

This means we check in local-plugins but when it comes to deployment and things we can safely ignore that folder as composer will run and copy across the files for us. Thankfully, it’s quite rare that plugins aren’t on either wpackagist or packagist. However, this approach can be useful for premium plugins or when you need to patch a plugin (to implement a bug fix for instance).

How we deal with premium plugins

Premium plugins offer a challenge because they aren’t listed on a public repository, usually, they are behind a paywall for a customer to download. Some of the popular ones offer a private repository, meaning you can still use composer. However, sometimes all you have is a zip file. In that case, we take the same approach as the local plugins above, only we put them in a folder called premium-plugins. Let’s look briefly though at what we do if the premium plugin does offer a private composer repository.

"repositories": [
  {
    "type":"composer",
    "url":"https://path/to/repository"
  }
]

We add a new repository to our composer.json pointing to the URL of the private repository offered by the plugin. We will have been given some credentials though which grants us access to the private repository. We put these into an auth.json file:

{
  "http-basic": {
    "path/to/repository": {
      "username": "{COMPOSER_API_USERNAME}",
      "password": "{COMPOSER_API_PASSWORD}"
    }
  }
}

What’s important here is this file never gets committed to GitHub (or any other VCS) due to the secrets for the username and password. We therefore have an auth.json.dist in our project base which shows the structure, and which developers can copy into auth.json (which we gitignore) to have working locally. We can use the encrypt-file feature of travis to enable this to work on CI and deployment (most CI runners have similar functionality).

One last thing on premium plugins which is really important to mention: READ THE TERMS. Often premium plugins can only be used by a restricted number of developers, in which case you need to make it clear which developers have access to the username and password of the private repository to ensure you are not breaking the terms of the purchase you made for that plugin.

Custom Code

WordPress is a rich ecosystem with lots of plugins that cover a myriad of things, however, we still need to write custom code. Our clients often engage us to deliver them a WordPress site that’s highly specific to their needs and although plugins can help fill in many gaps, there is still custom code required to ensure it exactly meets their requirements.

Custom code or bespoke development is what we excel at. It’s what we’ve done successfully on countless projects across multiple platforms. We know that some of the most important aspects of custom code are robustness, performance, reliability, and most important maintainability. Here are some of the rules we employ when writing custom code on WordPress.

Everything is a must-use plugin (mu-plugin)

Well, almost everything; sometimes it makes sense for things to be in the theme. Our golden rule is thus:

If I removed the theme should this piece of functionality still be available?

If the answer to the above is yes, then it should be in an mu-plugin.

WordPress has this weird quirk with mu-plugins in that when they are loaded, they only look for files and don’t descend into subfolders. Obviously, if we’re making heavy use of mu-plugins we aren’t going to have just single files, so we needed to make sure we could easily use directories of plugins here. We discovered this muplugin-loader library which did this job just fine, however, there were a few things we needed to update, so with the project falling into abandonment we forked and made some updates to suit our needs.

Scaffolding an mu-plugin

As we make heavy use of mu-plugins it becomes a bit cumbersome creating a new mu-plugin all the time, particularly as we have certain configuration requirements for each mu-plugin, to ensure it plays nicely with our DI container and our monorepo. Therefore, we provide a custom scaffolder for generating an mu-plugin, it’s called like this:

wp scaffold boxuk-mu-plugin sample-plugin

This creates the following files, with all the config setup for us:

  • sample-plugin.php with plugin header comment, and an example of calling the example service from the DI container.
  • composer.json with our autoload configuration in place.
  • config/services.yaml, service configuration for our DI container.
  • src/ExampleService.php, an example service class.
  • tests/ExampleTest.php, an example test class.

After this is called we need to merge the composer.json with the one in our root, which we do with bin/monorepo-builder merge, and update the composer autoloader composer dump-autoload. We wrap it all in a bash script for ease, so creating a new mu-plugin becomes as easy as:

bin/create-mu-plugin.sh sample-plugin

Dependency Injection

Dependency Injection (DI) is something we’ve become used to from other work we’ve done and when we first worked with WordPress we found ourselves a bit lost by not having a DI container at our disposal.

Symfony is our go-to framework for most of our PHP projects and it so happens to have an excellent DI container that can easily be used as its own component (as per every Symfony component). So it made sense for us to use this as our DI container of choice for our WordPress projects.

I won’t go into too much detail about the benefits of a DI container in this post, but in short, it allows us to easily manage objects and their dependencies. It means you only need to configure the dependencies of an object in one place, and retrieving from the container means you are guaranteed to have its dependencies met and ensures you are using the same instance throughout your application.

We’ve set up our container to look for services.yaml configuration files within the config directory in all of our mu-plugins.

We also allow plugins to register with the container so they can define their configuration wherever they wish. This works by convention currently and thus expects the extension file to be prefixed with BoxUk.

Finally, here’s an example of how we’d pull a service from our DI container.

$sample = boxuk_container()->get( 'BoxUk\Mu\Plugins\Sample\SampleClass' );

And the configuration for this service looks like the following:

services:
  _defaults:
    autowire: true
    autoconfigure: true

  BoxUk\Mu\Plugins\Sample\SampleClass:
    public: true

We use auto wiring and auto-configuration as standard but it can be considered optional.

Feature Flags

I wrote about feature flags a little over a year ago but essentially it allows us to not block deployment with work-in-progress features and gives us the power of being able to quickly and easily turn on or off features.

Once you start working with feature flags, it’s very hard to imagine a time where you didn’t use them. We use feature flags on all our projects and it’s allowed us to work with much greater efficiency. Continuous Delivery is something we aspire to and this is one important aspect of that.

For our WordPress projects we found a great plugin called flagpole. It works by registering flags in code, then providing an admin UI to toggle a flag on or off; finally through the flagpole_flag_enabled() function you can conditionally load a feature in the website or not. For example:

// Log some text in query monitor if the flag is enabled.
if ( flagpole_flag_enabled( 'log_sample_text' ) ) {
    do_action( 'qm/info', 'Log if feature enabled.' );
}

As we use feature flags so much, we wanted to lower the friction behind registering flags. We, therefore, provide a way of registering flags from a YAML file, for example:

feature_flags:
  log_sample_text:
    title: Log sample text
    description: Log sample text to query monitor
    enforced: false
    stable: true
    label: Samples

Themes

In all our projects we use a pattern library where we develop components which when sewn together can be used to build bigger components or entire pages. If you’re familiar with Brad Frost’s atomic design methodology, it’s not too dissimilar.

The primary tool we use for this is a templating engine called twig. On most of our PHP projects, our pattern library integrates seamlessly due to us also using twig as our templating engine for the app. However, WordPress doesn’t use twig as standard. Fortunately, there is a library called Timber that brings twig into WordPress. This allows us to use our pattern library components in our WordPress theme without too much trouble. It’s good because it means any updates to the pattern library will be straight away reflected in the main application.

We bring our pattern library into our theme as a subfolder and then use symlinking for the built assets for ease of linking to, and also for ease of deployment. It allows us to do things like {{ wp_enqueue_style(‘app.css’, ‘/assets/app.css’) }} as opposed to {{ wp_enqueue_style(‘app.css’, ‘/styleguide/public/dist/assets/app.css’) }}.

Bringing in our pattern library approach to WordPress themes was massive for us and we’re currently looking into how we can make it integrate nicely with the block editor.

Deployment

The final thing I just want to touch on is deployment. We have two main partners when it comes to WordPress: WordPress VIP and WP Engine. WordPress VIP gives you automated deployments from GitHub as standard. WP Engine does not but does give you the tools to do so. We really like the approach WordPress VIP offers so we added something to our travis builds for WP Engine projects that will deploy from GitHub much in the same way it works on WordPress VIP.

We generally work with three environments: development, staging and production. We then generally have branches that mirror this. Merging code into the development branch deploys to development, merging code into the staging branch deploys to staging, and so on. This means that deploying code is as easy as merging a pull request. We insist on the pull request as not only does it give us chance to conduct one final review (with certain VIP packages it allows their team to conduct extra reviews as well) but it also acts as a paper trail of deployments (commits alone will give you this paper trail, but it’s not as neatly laid out and you don’t get the meta-discussion you might on a PR).

We typically use the environments as thus:

  • Development: used mostly by our QA team to run automated and manual tests.
  • Staging: used mostly by our clients to trial new features, provide feedback and spot any issues we may have missed.
    Production: pretty self-explanatory!

Code is poetry

So there we are, a whistle-stop tour of how we develop on WordPress, there’s probably loads of details I’ve skimmed over so feel free to reach out on twitter, or you can find me loitering in the UK WordPress slack or the PostStatus slack.

It’s worth mentioning we’re hiring, so if any of the above has left you feeling like you’d like to work with this kind of setup, or have some ideas on improvements that could be made, or it’s made you feel that maybe WordPress development isn’t as gnarly as you thought. Then have a look at our job ad, and come and help us deliver some excellent WordPress solutions.

Discover how WordPress is powering frictionless digital experiences for the enterprise.

Book your free consultation

About the Author

Ian Jenkins

Ian Jenkins is a Principal Developer at Box UK. He has wide range of development experience in various platforms and languages, in particular PHP and Symfony. Ian has worked on and delivered a number of successful projects and is currently most interested in maintaining and transforming troublesome legacy projects into well-tested, high-performance web applications.