WordPress + Composer = Easier Development

WordPress + Composer = Easier Development

Composer is a PHP dependency manager that’s been in the wild for roughly 3.5 years. In short it helps mitigate the extremely antiquated ways PHP developers had been managing third party libraries.

We have been using Composer at StatenWeb since 2012 and it has significantly made our lives easier for development.

Composer is nothing new to PHP, as I said it’s over 3 years old, mainstream WordPress development with Composer has been slow to get on board. I’ve set up a workflow that is easy to use and fully takes advantage of simplistically managing third party libraries, including plugins (both premium and free from the wordpress.org repository) and libraries.

The goal of this article is to be a reference and I am going to keep it as concise as possible but some of this material requires a thorough examination and introspection.

Composer Basics

Composer is a command line utility written in PHP. To get started follow this guide, be sure to install it globallyinstructions on their official homepage. It should take you two commands to be up and running on *NIX environments (Linux or OS X), YMMV on Windows.

The crux of composer lies in the composer.json file, which is a manifest and set of instructions that tells composer which (including version numbers), how and where to install dependencies (referred to as packages, libraries, plugins among other terms). Once you create your composer.json simply run composer update. This goes out and grabs your libraries and installs them where you specify, into a vendor directory by default. This also creates a composer.lock file which is a representation of the exact versions of packages installed which locks the project to the specific versions.

A dependency is any code that your project takes advantage of but is not maintained by your project. To understand this better, think of your WordPress project as the theme. You are creating a theme and the theme depends on WordPress itself along with plugins and possibly libraries.

Sidebar: Composer provides autoloading of packages out of the box. Autoloading is not the subject of this post per se, but think of it as a single PHP file that automatically includes your dependencies. Luckily WordPress’ Must Use Plugins functions in that WordPress only looks for PHP files right inside the mu-plugins directory, and (unlike for normal plugins) not for files in subdirectories., this is exactly how Composer’s autoloader is built to function. In short, WordPress’ Must Use Plugins directory executes any script in its root directory (which defaults to wp-content/mu-plugins). We will instruct composer to install any libraries (essentially PHP scripts that re libraries and not plugins or WordPress itself) in our mu-plugins directory. There is a significant chance that you will not need this or, if you do, you won’t need to know how it works, which is, set it and forget it; but I figured a little background cannot hurt.

WordPress is a Dependency

When we started down this path at StatenWeb, one of the toughest concepts to wrap our heads around was that WordPress is a depenency of our project. Furthermore the concept that our project was really the theme itself – when delivering client sites, typically we weren’t building WordPress, we weren’t building plugins – so we shouldn’t worry about them. Composer provided the tool to realize this. As we went down this path we realized that our git repositories did not need to include third party code (WordPress itself, any plugins and libraries). This was extremely liberating and this guide can help liberate you as well.

WordPress in its own directory

The old way we built WordPress sites was to upload (via FTP, [the horror ;)] ) something like the below:

/projectroot/
..............wp-config.php
..............wp-admin/
..............wp-includes/
..............wp-content/
.........................themes/
................................our-theme/
.........................plugins/
.................................random-plugin/
.................................random-plugin2/
.........................mu-plugins/
....................................mu-plugin-file.php
.........................uploads/
.................................2015

All in one fell swoop. If we wanted to update WordPress we would have to do it automatically via the UI or make the change locally using MAMP and re-uploading everything except the wp-content directory. This can be quite messy and you could easily go out of sync with your git repository and your audit trail for your code becomes unwieldy.

For our first step in streamlining our process, WordPress, luckily, gives us a way to install in its own directory. We can begin decoupling our code from WordPress’ core, eventually having this file structure:

/projectroot/
...............web/
...................wp-config.php
...................app/
.......................themes/
..............................our-theme/
.......................plugins/
..............................random-plugin/
..............................random-plugin2/
.......................mu-plugins/
...................wp/
...................uploads/

Some things to note:

  • We will tell our HTTP server to look in projectroot/web/ as our root. This will allow us to add Capistrano configuration in the root for super simple deploys. This will come in a later article.
  • our app/themes directory now becomes the location of our project’s code
  • wordpress is now installed in the directory wp/
  • we’ve moved our uploads directory up to the root (this is additional sugar and can be accomplished using a plugin)
  • We technically have not moved our wp-config.php file but we have moved the path that it was in, which was the WordPress core root. Luckily WordPress checks one level above your WordPress core directory, so this will work in our example.

Our .gitignore for this project will contain the Composer controlled packages, so it should look something like:

web/wp/
web/app/plugins
web/app/mu-plugins/
web/uploads/

So our git repo would only be something like this:

/projectroot/
...............web/
....................gitignore
...................wp-config.php
...................composer.json
...................composer.lock
...................app/
.......................themes/
..............................our-theme/

Notes:

  • you may be asking why we are versioning our wp-config.php file when it contains sensitive database credential information. We will get to that later.

I Thought This Was About Composer?

So, now that we’ve laid the groundwork. Let’s dive in. Our finished composer.json will look something like the following:

{
  "name": "statenweb/statenweb",
  "description": "wordpress setup for our staten island web design project",
  "license": "proprietary",
  "repositories": [
    {
      "type": "composer",
      "url": "http://wpackagist.org"
    }
  ],
  "require": {
    "johnpbloch/wordpress": "4.3.1",
    "jublonet/codebird-php": "~2.7",
    "wpackagist-plugin/wp-original-media-path": "~1.4",
    "wpackagist-plugin/wordfence": "~6.0",
    "mnsami/composer-custom-directory-installer": "1.0.*",
    "vlucas/phpdotenv": "~2.0"
  },
  "extra": {
    "wordpress-install-dir": "./web/wp",
    "installer-paths": {
      "./web/app/plugins/{$name}/": [
        "type:wordpress-plugin"
      ],
      "./phpdotenv/": ["vlucas/phpdotenv"]
    }
  },
  "config": {
    "vendor-dir": "./web/app/mu-plugins"
  }
}

Let’s break this down.

The name, description and license keys are best explained on Composer’s documentation.

The repositories key gives us a chance to define custom packages or package sources. Out of the box, Composer automatically adds packagist.org as a source for packages. That is, when Composer is searching for our project’s dependencies, unless we specify additional repositories, Composer will look for the packages in packagist.org. We add in wpackagist.org as well. Wpackagist is an extremely useful project that automatically makes WordPress plugins (and themes) available as Composer packages with a type of wordpress-plugin. By adding it to the repositories key, Composer will search in wpackagist as well as packagist for dependencies. In later articles I will show you how to add custom packages from git sources, zip files and others, which is helpful for premium and custom plugins.

In the require key we specify our packages that we want to use in our project. The first package, johnpbloch/wordpress is a package from packagist.org that mirrors the WordPress core and it also includes the ability to install WordPress in a custom location. More on this later. After listing the dependency’s namespace/name you need to specify the version. You can set an exact version, next sigificant release versions, wildcard versions and more. See [https://getcomposer.org/doc/articles/versions.md](Composer’s documentation on versions) for more information.

The next package jublonet/codebird-php is a library for connecting to Twitter. This is just a basic example of a non-WordPress-specific library that we want to use in our project and how we can include it with Composer. This is a package with the default Composer package type (which is library) that we want installed in mu-plugins we will configure how this works when we break down the extra key. Note we specify a tilde version for this and the subsequent packages.

The next two packages are packages from wpackagist.org. All that you need to specify the slug from the wordpress.org repository. For example, WP Original Media Path has a slug of wp-original-media path and Wordfence Security has a slug of wordfence. We speicfy the tilde version here as well.

The next two packages we haven’t yet discussed. The first mnsami/composer-custom-directory-installer allows us to install a package anywhere without our application. We use this to install vlucas/phpdotenv in the root of our project.

I won’t go into details about PHP Dot Env; however, it allows us to specify a special configuration file (.env) that contains environment variables. For our project this will include database name, login and password information along with URL information This file does not get versioned. This allows us to version our wp-config.php which will reference these environment variables. I will cover this in more detail below.

In the extra key we specify installation locations. As referenced above, johnpbloch/wordpress pulls in a package that lets us specify where we want to install WordPress, in this case, it’s ./web/wp. Then we set our installer-paths using Composer’s native installation methods. As outlined above we tell Composer to install plugins of type:wordpress-plugin. Next, we want a specific package vlucas/phpdotenv installed in ./phpdotenv – I will explain the rationale later on. Last, in the config key we tell Composer to install default packages in ./web/app/mu-plugins.

Let’s Compose, Baby

We’re all set. Copy and paste the composer.json and .gitignore above (or even easier I made a gist into a new folder and run composer install.

This turns your project from

/projectroot/
..............gitignore
.............composer.json

into

/projectroot/
..............gitignore
.............composer.json
.............composer.lock
.............phpdotenv/
.............web/
.................app/
.....................plugins/
.............................wordfence/
.............................wp-original-media-path/
.....................mu-plugins/
................................composer/
................................johnpbloch/
................................jublonet/
................................mnsami/
................................autoload.php
.................wp/
....................{all of WordPress' core files)

PHP Dot Env is loaded outside of the vendor directory (mu-plugins) by design. The mu-plugins directory contains all of Composer specific packages + an autoloader. WordPress is installed in the wp directory and the two actual WordPress plugins are installed in the plugins directory.

We have four files that we need to add to this installation.

  1. A .env.example serves as an example of how our .env file is structured.
  2. A .env file that contains our environment specific configurations. This should be added to the .gitignore file.
  3. A wp-config.php required by WordPress.
  4. A index.php required by WordPress, our entry point for our application.

.env.example

Let’s start with the .env.example file. Both the .env and .env.example files should be structured as KEY=VALUE. For example, let’s populate it like so:

DB_NAME=database_name
DB_USER=database_user
DB_PASSWORD=database_password
DB_HOST=database_host

WP_ENV=environment
WP_HOME=http://example.com
WP_SITEURL=http://example.com/wp

This is fairly straightforward, we populate our database name, user, password, host and then the WordPress environment, the home URL and the location of our WordPress installation.

.env

Let’s put in our environment’s actual variables. I am using a Vagrant box that has a database name: staten-island-web-design user: root password: root and a host of localhost. THis site is going to live on http://test.statenweb.com and wordpress will live at http://test.statenweb.com/wp – so my file will look like such:

DB_NAME=staten-island-web-design
DB_USER=root
DB_PASSWORD=root
DB_HOST=localhost

WP_ENV=development
WP_HOME=http://test.statenweb.com
WP_SITEURL=http://test.statenweb.com/wp

Let’s add this to our .gitignore, which now looks like:

.env
web/wp/
web/app/plugins
web/app/mu-plugins/
web/uploads/

index.php

This one is simple, we just need to make a single change to the default WordPress index.php

<?php
/**
 * Front to the WordPress application. This file doesn't do anything, but loads
 * wp/wp-blog-header.php which does and tells WordPress to load the theme.
 *
 * @package WordPress
 */

/**
 * Tells WordPress to load the WordPress theme and output it.
 *
 * @var bool
 */
define('WP_USE_THEMES', true);

/** Loads the WordPress Environment and Template */
require( dirname( __FILE__ ) . '/wp/wp-blog-header.php' );

Notice we are adding /wp to the require call to wp-blog-header.php. This complies with our installing WordPress in a sub directroy.

wp-config.php

<?php

require __DIR__ . '/../phpdotenv/src/Dotenv.php';
require __DIR__ . '/../phpdotenv/src/Loader.php';
require __DIR__ . '/../phpdotenv/src/Validator.php';

$dotenv = new Dotenv\Dotenv( dirname( __DIR__ ) );
$dotenv->load();
$dotenv->required( array( 'DB_NAME', 'DB_USER', 'DB_PASSWORD', 'DB_HOST', 'WP_ENV', 'WP_HOME', 'WP_SITEURL' ) );


// ** MySQL settings ** //
/** The name of the database for WordPress */
define( 'DB_NAME', $_ENV['DB_NAME'] );

/** MySQL database username */
define( 'DB_USER', $_ENV['DB_USER'] );

/** MySQL database password */
define( 'DB_PASSWORD', $_ENV['DB_PASSWORD'] );

/** MySQL hostname */
define( 'DB_HOST', $_ENV['DB_HOST'] );

/** Database Charset to use in creating database tables. */
define( 'DB_CHARSET', 'utf8' );

/** The Database Collate type. Don't change this if in doubt. */
define( 'DB_COLLATE', '' );

define('AUTH_KEY',         '1.39D^[Q2+3vCSMt6.lKlk6</-f6HJJ4>Aq|YL.+jDx*4mg#-G^nBF[lC$]rM]|.');
define('SECURE_AUTH_KEY',  'Le8.W>y*<|-00&LJEcb+OF<%|x%Kx(hQ4A>3]dB5@rc`qCrn#7,/3+A#(qUptr@l');
define('LOGGED_IN_KEY',    'EpR70Kxwg8C+<=<hr:+:nEP;,hujmIR|I$Cf^N:>$PN<RN#>[Ie5`N/BeWg[%m*E');
define('NONCE_KEY',        'A[!ewjUCz_h5fU-G~E-+[w^-{`cUU><XI@!n?-;9C3D1ec{u^4BLjcGEF73Tvye4');
define('AUTH_SALT',        '`%5_Yvy)U+aB]cg;N1V-fG<!9dKq9g5i)*MTMZuMs|f+p^&$KR<ocbwZGe(u9>5H');
define('SECURE_AUTH_SALT', 'f~Ny*wfznlQN`hRe[_.deI]%-2=>6Nt;@<|<*:tU@JWP|([tmeN@aOh~rg}DmHya');
define('LOGGED_IN_SALT',   '*+RA;)XUr?D& + U7Ua-&`^NHN]&sIBowBO9|4ryIXa|>3*T1E **J^N@}Ua17B-');
define('NONCE_SALT',       '>dw[3gRP]M:!bV^n:ykfHe:p(Q^~MT3yI(@>@A&s)@ZVHIi+}V-RdYo<w6J5O#rq');

$table_prefix = 'wp_';

define( 'WP_CONTENT_DIR', dirname( __FILE__ ) . '/app' );
define( 'WP_CONTENT_URL', 'http://' . $_SERVER['HTTP_HOST'] . '/app' );
define( 'WPMU_PLUGIN_DIR', dirname( __FILE__ ) . '/app/mu-plugins' );
define( 'WPMU_PLUGIN_URL', 'http://' . $_SERVER['HTTP_HOST'] . '/app/mu-plugins' );

define( 'WP_HOME', $_ENV['WP_HOME'] );
define( 'WP_SITEURL', $_ENV['WP_SITEURL'] );

/* That's all, stop editing! Happy blogging. */

/** Absolute path to the WordPress directory. */
if ( ! defined( 'ABSPATH' ) ) {
    define( 'ABSPATH', dirname( __FILE__ ) . '/' );
}

/** Sets up WordPress vars and included files. */
require_once( ABSPATH . 'wp-settings.php' );

There’s a lot going on here.

  • Since we specificed a special location for PHP Dot Env, we have to include the library manually, next this code:

$dotenv = new Dotenv\Dotenv( dirname( DIR ) );
$dotenv->load();
$dotenv->required( array( ‘DB_NAME’, ‘DB_USER’, ‘DB_PASSWORD’, ‘DB_HOST’, ‘WP_ENV’, ‘WP_HOME’, ‘WP_SITEURL’ ) );

Initializes the class telling it to look for the .env one level up and executes the load method. Next it tells the library that settings in the .env file must contain values DB_NAME, DB_USER, DB_PASSWORD, DB_HOST, WP_ENV, WP_HOME, WP_SITEURL. These variables automatically populate as array keys in the $_ENV superglobal and instantly become available throughout the application.

We can now populate their corresponding values throughout our wp-config.php file, as seen in:

/** The name of the database for WordPress */
define( 'DB_NAME', $_ENV['DB_NAME'] );

/** MySQL database username */
define( 'DB_USER', $_ENV['DB_USER'] );

/** MySQL database password */
define( 'DB_PASSWORD', $_ENV['DB_PASSWORD'] );

/** MySQL hostname */
define( 'DB_HOST', $_ENV['DB_HOST'] );

Next we use WordPress’ autogenerator of salts and add/replace the defaults.

Then we tell WordPress where our plugin directories and corresponding URLs are located based on the location current file (i.e. wp-config.php).

define( 'WP_CONTENT_DIR', dirname( __FILE__ ) . '/app' );
define( 'WP_CONTENT_URL', 'http://' . $_SERVER['HTTP_HOST'] . '/app' );
define( 'WPMU_PLUGIN_DIR', dirname( __FILE__ ) . '/app/mu-plugins' );
define( 'WPMU_PLUGIN_URL', 'http://' . $_SERVER['HTTP_HOST'] . '/app/mu-plugins' );

Finally, we tell WordPress what the URL root of our site (WP_HOME) and URL location of WordPress WP_SITEURL.

define( 'WP_HOME', $_ENV['WP_HOME'] );
define( 'WP_SITEURL', $_ENV['WP_SITEURL'] );

We’re almost there. Our new repo should look like the following:

/projectroot/
..............gitignore
..............env.example
.............composer.json
.............web/
.................wp-config.php
.................index.php

Finally, let’s add our theme, for example, let’s generate one called Staten Island Web Design using our favorite starter theme generator at Underscores into /web/app/themes

Finally, our repo looks like:

/projectroot/
..............gitignore
..............env.example
.............composer.json
.............web/
.................wp-config.php
.................index.php
.................app/
.....................themes/
............................staten-island-web-design/

Let’s run composer install.

Our project is built! Point your HTTP server, either using Vagrant, MAMP/ MAMP Pro (don’t use MAMP Pro ;)) or your operating system’s built in HTTP / PHP.

Future articles that I will write will include:

  • Requiring premium WordPress plugins
  • Rolling your own composer respository (like packagist and wpackagist)
  • Using Vagrant to speed up local development
  • Capistrano and One Command Deploys

Let me know if you have any questions or comments in the comment section below.