Avoiding multiple composer installs with Sage/Bedrock

At the agency I work for, much as in any PHP shop, we do a fair amount of WordPress builds and maintenance. The WP codebase is ancient, and is about as far removed from today’s Laravel/Symfony best practises as it can be.

Having said all that, it’s ubiquitous and of course still very much the most popular CMS in the world, and, to its credit, is very well-documented. A year spent working in the trenches on Magento 2 builds and support - which has mind-crushingly terrible documentation - taught me to re-evaluate WP, appreciate its docs and forgive its many legacy quirks. Relentless global scoping and bizarre loop syntax FTW!

Nevertheless, I started researching and looking at devising a better WP workflow to use on green field projects to make the development experience a little bit more enjoyable. This inevitably led me to Roots’ Bedrock and Sage. If you don’t know them already, they are, respectively, WordPress project and theme frameworks which follow modern PHP development practices: OOP, templating via Blade, php package management - including plugins and themes - via composer, frontend asset management via yarn, a well organised app folder, dotenv, and much much more. Awesome, this meant I wouldn’t have to re-invent the wheel! So I started playing around with a couple of test projects.

Roots Homepage

Bedrock installation was extremely straightforward, set up your database, composer create-project, and edit your .env file. I usually use Valet+ for my local development environment, and the great news is that even vanilla Valet, which it’s forked from, ships with a Bedrock driver out of the box, so no need to write a custom driver. It just works. Result.

I usually keep any themes and plugins I’m working on in their own separate repo directories, and symlink them into the local WordPress install during dev. For production deployment, I figured I might want to then include the theme and/or plugins as Bedrock project composer dependencies along with the third party stuff.

This way, on a typical project, I might have a Bedrock project/app repo, a theme repo based on Sage, plus any additional custom plugin repos (more on my current approach to WordPress plugin development in a future post)

Arguably, and this is actually what the Bedrock docs suggest, you could also include the main project theme as part of your project repo rather than keeping it separate. That might well make sense, themes are often speciic to the project in any case.

Whichever approach you take though, I found a couple of considerations to bear in mind.

Firstly, the Sage theme framework docs suggest running composer install from within the theme directory. This seems to me to be an odd design choice and potentially problematic - this would mean running multiple composer installs (e.g. within your plugins, themes and main project directories), and therefore multiple vendor folders and autoloads, bringing with it the risk of hard to debug package version conflicts.

Composer docs are fairly clear on this. It’s standard in, for example, Laravel or Symfony apps to run a single composer install from your main app directory. That way you have a single project vendor dir and autoloader.

You could therefore just simply include your themes or plugins via composer from the Bedrock/Wordpress project root and composer will do the rest, but what about during the dev/symlinking phase that I mentioned above? How will the Bedrock install know where to look for plugin and theme composer.json files? I took a quick look at October CMS (awesome Laravel-based CMS, which I’ve used previously on a couple of projects, and which you should definitely try out if you haven’t already) to see how they solve a similar problem for their plugin and theme ecosystem - Wikimedia’s composer-merge-plugin to the rescue.

I ran the following in my project directory:

$ composer require wikimedia/composer-merge-plugin

Then added the next snippet to my composer.json

  "extra": {
        // ...
        "merge-plugin": {
            "include": [
                "web/app/plugins/*/composer.json",
                "web/app/mu-plugins/*/composer.json",
                "web/app/themes/*/composer.json"
            ],
            "recurse": true,
            "replace": false,
            "merge-dev": false
        }

Then ran $ composer update

So far so good! Composer found the relevant theme and plugin composer.jsons and all their dependencies were installed into the main project vendor folder.

But that wasn’t quite the end of it - loading Sage-based themes’ composer dependencies from the main bedrock directory is not quite as straightforward as expected. I got the following error viewing the test site with the theme enabled:

Fatal error: Uncaught Symfony\Component\Debug\Exception\FatalThrowableError: Class 'App' not found ....

After the usual google/stack overflow dance, I found out that this is related to the order in which Controller loads versus WordPress, if included in the main app vendor folder rather than the theme folder, which is what we are doing here. A workaround mu-plugin solution is detailed here.

I left out the debugger line so my final fix looked like this:

<?php
// web/app/mu-plugins/controller-start.php
// add Controller to WP init so it's available in time
add_action('init', 'Sober\Controller\loader');

It’s working! With this mu-plugin in place, I’m now all set for development.

Sage running on a local Bedrock install

One final tip - once you are at the stage of including your own plugins or themes in your project as composer dependencies, you’ll need to ensure that they have their “type” set properly in their composer.json, so that Bedrock places them in the correct folder.

// add either of the following to your theme/plugin composer.json
{
    "type": "wp-plugin", // plugin
    "type": "wp-theme" // theme
}

Overall, I’m loving the feel of both Sage and Bedrock so far - they seem to provide the best development experience for WordPress right now, and I’m looking forward to using them both in earnest in a production app.