Skip to content

Instantly share code, notes, and snippets.

@gemmadlou
Last active November 12, 2024 22:06
Show Gist options
  • Save gemmadlou/6fc40583318430f77eda54ebea91c2a1 to your computer and use it in GitHub Desktop.
Save gemmadlou/6fc40583318430f77eda54ebea91c2a1 to your computer and use it in GitHub Desktop.
WordPress Composer Starter (Steps)

Develop WordPress as a Modern PHP Project with Composer


WordPress is popular because it's easy to setup without much technical know-how. However, to build a more robust PHP project with command line deployments, updates and ongoing maintenance, working with WordPress out-of-the-box raises specific challenges:


  • How can we make our WordPress projects portable between developers?

  • How do we synchronise our codebases with production and prevent users from creating inconsistencies between environments?

  • How do we deploy faster and more reliably?

  • How can we benefit from libraries and frameworks used by the wider PHP community?


This article not only answers these questions, but it us shows how to resolve these problems by recreating the WordPress Composer Starter.


This tutorial is broken down into 6 sections:


  1. How to install WordPress with Composer

  2. Structuring a WordPress Composer project

  3. Controlling WordPress configurations

  4. Managing different environments easily

  5. Installing plugins and themes with WPackagist

  6. Keeping the repository clean

# Pre-requisites


  • PHP

  • Composer (download)

  • Text Editor - IDE

I currently use Visual Studio Code

  • A server that has MySQL, PHP and Apache/Nginx

# How to install WordPress with Composer


Why use Composer in the first place?

Sorry if you're already sold on Composer. If you're new to it, you'll love what you can achieve:

  • Once we have a Composer file, we can share it with fellow developers or use it as a starter for other projects.

  • For a project, WordPress core, plugin and theme versions across all environments, development and production will be the same.

  • Config dependencies

  • Clean version control


For instance, we could just grab a WordPress zip file from the internet and manually download it. But that takes time. Worse, if I develop a theme with Wordpress 4.5, but someone else in my team uses an already downloaded version and starts developing the same project with 3.7, there are bound to be inconsistencies.



And why install WordPress as a project dependency?

WordPress is written in such a way that we extend it and add custom functionality without ever touching the WordPress core. That's why there are add_action and add_filter hooks throughout the codebase. There is no reason why we need to treat the WordPress core as project-specific code.



# Create the config file

Let's create a new folder:

$ mkdir WordPress-Composer-Starter \
	&& cd WordPress-Composer-Starter



Create the Composer configuration composer.json file:

$ composer init

Note: you'll need to

  • enter the name
  • enter the description
  • press Enter to skip any prompts you're not ready to answer yet



At the end of the Composer initialisation, your Composer file may look like this:

> composer.json
{
  "name": "gemmablack/wordpress_composer_starter",
  "description": "WordPress Composer Starter repository for quick-starting WordPress projects",
  "authors": [
      {
          "name": "Gemma Black",
          "email": "[email protected]"
      }
  ],
  "require": {}
}



# Add WordPress as a project dependency

We need to add the following require block option to the Composer configuration file.

> composer.json
{
  "require": {
    "johnpbloch/wordpress": "~4"
  }
}



Managing WordPress versions

We can say exactly what WordPress versions we want. For now, we've simply stated that we want the latest files within WordPress version 4, marked by the tilda sign '~'. We can specify specific versions instead like "johnpbloch/wordpress": "4.7.5".


To add all the WordPress files to the project, simply run:

$ composer update



# Directory structure after the Composer update

Just to check. Your directory should look like this:

WordPress-Composer-Starter
  |-- vendor /			# Normal Composer packages folder
  |-- wordpress /		# WordPress core files
  |-- composer.json
  |-- composer.lock		# Created by Composer



# WordPress core and the vendor folder

How does the WordPress core load into the root of the project and not the vendor folder?


First, note the following response from your terminal after the composer update:


> Loading composer repositories with package information
> Updating dependencies (including require-dev)
> Package operations: 3 installs, 0 updates, 0 removals
>  - Installing johnpbloch/wordpress-core-installer (1.0.0.2): Loading from cache
>  - Installing johnpbloch/wordpress-core (4.9.4): Loading from cache
>  - Installing johnpbloch/wordpress (4.9.4): Loading from cache

You might be thinking, "I only required one package". That's correct. But your required package has requirements of it own.


The John P Bloch wordpress package has a Composer file which references two additional packages:

  1. The johnpbloch/wordpress-core-installer is a custom Composer plugin that installs WordPress outside the vendor folder.

  2. The johnpbloch/wordpress-core is a Composer package that has the WordPress core files tagged by versions.



# Structuring a WordPress Composer project


Before continuing, it's good to talk about the structure of the WordPress project. So far, the default location of our WordPress folder is directly in the root. We'll want to move it to another directory which we'll call public. The benefit is that we can hide private or sensitive information outside of the public folder, which cannot be accessed via a url.

We also want to separate the wp-content folder and code unique to the project from the WordPress core.



Here's the project structure:

root
  |-- composer.json
  |-- composer.lock
  |-- public
    |-- wordpress       # From John P Bloch's project
    |-- wp-content      
       |-- themes       # Mixture of repository themes and loaded via composer
       |-- plugins      # Mixture of repository plugins and loaded via composer
       |-- uploads      # Uploads folder, ignored by the repository (via .gitignore)
  |-- vendor            # Composer packages folder

So how will we achieve this?



Change the default WordPress directory

All we have to do is add the following to the Composer file:

> composer.json
{
  "extra": {
    "wordpress-install-dir": "public/wordpress"
  }
}

Note: The WordPress installer Composer by John P Bloch allows WordPress packages to be installed outside the vendor folder. It allows allows for custom directories also.



To install it again, run:

$ composer update



Delete the old WordPress directory

You'll now find WordPress in the public/wordpress folder. You'll also find the old folder is still there directory in the route which should be removed:

$ rm -rf wordpress # You may have to use sudo privileges



Add the theme, upload and plugin folders

$ mkdir public/wp-content \
	&& mkdir public/wp-content/themes \
	&& mkdir public/wp-content/uploads \
	&& mkdir public/wp-content/plugins

Note: To preserve the folder structure in Git whilst empty, just add a .gitkeep file to each folder.

# Controlling WordPress' configurations


WordPress will not run anymore as there isn't an index.php in our public folder.

So we need to tell WordPress:

  • where the themes are located
  • where the plugins are located
  • the new uploads directory



# Pointing to the WordPress subfolder

We need to create a copy of the WordPress install index.php file so that it lives within the root of the public directory.

$ touch index.php

> public/index.php
<?php
/**
 * Front to the WordPress application. This file doesn't do anything, but loads
 * 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__ ) . 'wordpress/wp-blog-header.php' );



# Create a custom WP Config

If we were to setup the database and webserver, then go to our new WordPress website, we could start the famous "5 Minute Install".

The problem is that it would ignore our unique folder structure. Hence, we need to manually create the wp-config.php file.



$ touch public/wp-config.php

Note: this file is not included by default with WordPress downloads or even Composer install. So it has to be created, either manually or using WordPress' famous "5 Minute Install".



Configure the database

> public/wp-config.php
<?php

/**
 * Database name
 */
define('DB_NAME', 'wordpress');

/**
 * Database user
 */
define('DB_USER', 'root');

/**
 * Database user password
 */
define('DB_PASSWORD', 'root');

/**
 * Database host
 */
define('DB_HOST', 'localhost');

/**
 * WordPress DB Charset (is setup this way when the tables are made)
 */
define( 'DB_CHARSET', 'utf8' );

/**
 * WordPress DB Collation (is setup this way when the tables are made)
 */
define( 'DB_COLLATE', 'utf8_general_ci' );



Correcting the URLs

We access the WordPress admin through the wp-admin/ folder. Hence, WP_SITEURL goes through the /wordpress route, whereas WP_HOME goes to the home domain.

> public/wp-config.php
<?php

// >> add

/**
 * WordPress home URL (for the front-of-site)
 */
define('WP_HOME', 'http://' . $_SERVER['HTTP_HOST'] . '');

/**
 * WordPress site URL (which is for the admin)
 */
define('WP_SITEURL', 'http://' . $_SERVER['HTTP_HOST'] . '/wordpress');



Custom content directories

Our wp-content directory is not within the Wordpress core folder, and neither is the plugins directory, so we have to manually set those.

Additionally, the themes folder is always relative to the WP_CONTENT_DIR, so we do not need to set the themes folder location.

> public/wp-config.php
<?php

// > etc

/**
 * WordPress content directory
 */
define('WP_CONTENT_DIR', dirname(__FILE__) . '/wp-content');

/**
 * WordPress plugins directory
 */
define('WP_PLUGIN_DIR', dirname(__FILE__) . '/wp-content/plugins');

/**
 * WordPress content directory url
 */
define( 'WP_CONTENT_URL', 'http://' . $_SERVER['HTTP_HOST'] . '/wp-content' );



Setting Up Error Logging

On production, we don't want error messages to leak out to the user, but we still want error logging on local or staging environments. On local, we may want to see all error logs on the screen:

> public/wp-config.php
<?php

// >> etc

/**
 * Controls the error reporting. When true, it sets the error reporting level
 * to E_ALL. 
 */
define( 'WP_DEBUG', true );

/**
 * If error logging is enabled, this determines whether the error
 * is logged or not in the debug.log file inside /wp-content.
 */
define( 'WP_DEBUG_LOG', true );

/**
 * If error logging is enabled, this determines whether the error is
 * shown on the site (in-browser)
 */
define( 'WP_DEBUG_DISPLAY', true );



Stopping Users From Altering Themes & Plugins

Of course, now we are using Composer to install plugins and themes, we really don't want users being able to update, delete, or add any untested plugins and themes without going through the proper process.

It would not be helpful that our project on production contains code that a user has added that is not in our code base. Testing would be difficult, but more than that, tracking down bugs would take considerably longer.

> public/wp-config.php
<?php

// > etc etc

/**
 * This disables live edits of theme and plugin files on the WordPress
 * administration area. It also prevents users from adding, 
 * updating and deleting themes and plugins.
 */
define( 'DISALLOW_FILE_MODS', true );

/**
 * Prevents WordPress core updates, as this is controlled through
 * Composer.
 */
define( 'WP_AUTO_UPDATE_CORE', false );



WordPress table prefix

The default for WordPress is to prefix each table name with wp_. Some have considered it a little extra secure to change the table prefix to something random. However, the table prefix needs to be included here regardless.

$table_prefix = 'wp_';



Authentication keys and salts

You can generate the salts on https://api.wordpress.org/secret-key/1.1/salt/. This adds an extra layer of security to some WordPress security actions. It already generates a salt in the database, but having them in the WP Config adds an extra layer of security.

> public/wp-config.php
<?php

// > etc

/* Authentication Unique Keys and Salts. */
/* https://api.wordpress.org/secret-key/1.1/salt/ */
define( 'AUTH_KEY',         'put your unique phrase here' );
define( 'SECURE_AUTH_KEY',  'put your unique phrase here' );
define( 'LOGGED_IN_KEY',    'put your unique phrase here' );
define( 'NONCE_KEY',        'put your unique phrase here' );
define( 'AUTH_SALT',        'put your unique phrase here' );
define( 'SECURE_AUTH_SALT', 'put your unique phrase here' );
define( 'LOGGED_IN_SALT',   'put your unique phrase here' );
define( 'NONCE_SALT',       'put your unique phrase here' );



The absolute path to the WordPress directory

WordPress needs to load files starting from the public folder, not the root of the project.

> public/wp-config.php
<?php

// > etc

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

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



# Finally, ensure routing is correct

You will need to log into the WordPress dashboard and update the permalinks there. This generates the .htaccess for public folder.

# Managing different environments


At the moment, managing different environments will be cumbersome. We have database configurations hardcoded inside the wp-config file. We should not be reliant on version control to set up different environments.

Environment files are thus a great language-agnostic solution with the added benefit of being able to hide away sensitive and private information (considering .env files are not commited to version control).

This section is a short example of how to install a typical Composer package that has nothing to do with the WordPress ecosystem.



# Add the DotEnv package to WordPress

$ composer require vlucas/phpdotenv

This automatically updates the composer.json and composer.lock files too.


Require the Composer autoload file which will give us access to the DotEnv package.

> public/wp-config.php
require __DIR__ . '/../vendor/autoload.php';



# Take the Database configurations from the environment file

First, initialise the Dotenv package:

> public/wp-config.php
<?php

$dotenv = new Dotenv\Dotenv(__DIR__ . '/../');
$dotenv->load();



Make the database configurations dynamic:

> public/wp-config.php
<?php
/**
 * Database name
 */
define('DB_NAME', getenv('DATABASE_NAME'));

/**
 * Database user
 */
define('DB_USER', getenv('DATABASE_USER'));

/**
 * Database user password
 */
define('DB_PASSWORD', getenv('DATABASE_PASSWORD'));

/**
 * Database host
 */
define('DB_HOST', getenv('DATABASE_HOST'));



# Dynamic Security Salts

Have .env controlled security salts. The added benefit is that the .env file is not inside the WP Config file, and cannot accidentally be found via a public url.

> public/wp-config.php
<?php
  
/* Authentication Unique Keys and Salts. */
/* https://api.wordpress.org/secret-key/1.1/salt/ */
define('AUTH_KEY',         getenv('AUTH_KEY'));
define('SECURE_AUTH_KEY',  getenv('SECURE_AUTH_KEY'));
define('LOGGED_IN_KEY',    getenv('LOGGED_IN_KEY'));
define('NONCE_KEY',        getenv('NONCE_KEY'));
define('AUTH_SALT',        getenv('AUTH_SALT'));
define('SECURE_AUTH_SALT', getenv('SECURE_AUTH_SALT'));
define('LOGGED_IN_SALT',   getenv('LOGGED_IN_SALT'));
define('NONCE_SALT',       getenv('NONCE_SALT'));



# Create a sample Environment file

You'll want a sample file that can copied by other developers for their local setup, and that also can be configured for use on other environments.

$ touch .env.example



This environment file shouldn't contain any sensitive information, and should allow developers to simply copy, and create an .env file as and when they need.

> .env.example
DATABASE_NAME="wordpress"
DATABASE_USER="root"
DATABASE_PASSWORD="root"
DATABASE_DATABASE_HOST="localhost"

AUTH_KEY=
SECURE_AUTH_KEY=
LOGGED_IN_KEY=
NONCE_KEY=
AUTH_SALT=
SECURE_AUTH_SALT=
LOGGED_IN_SALT=
NONCE_SALT=



# Configure your project's .env file

Copy the sample environment file and create a .env file. This will have sensitive information, so make sure your version control ignores this file as you do not want to commit this accidentally to a repository.



# Generate salts quickly

Go to https://roots.io/salts.html. It will automatically generate salt environment keys. Copy the environment style keys and paste it into the environment file:

> .env
AUTH_KEY='-NhvP0TTq?G!b&`5stTph@+:gt45^SH(YPTVCqG`6jB42qN3tqb!RN2Gu4EO`&&u'
SECURE_AUTH_KEY='nCn:.EVF#dinHGMM9uYIn){4cgscEh}evwdDrH=/(u(#Q-!EJwj5]TVM}dcMPy2b'
LOGGED_IN_KEY='Y=$sP12).I&j}Ih6@6vf,k$,+dqe:nO9q;#Kyo!AzDgmz!g+Y=$Tusak``4uf;/q'
NONCE_KEY=',T>)a,.VpYDRW!tI6L)B.NF2134Qcq!v6?*7YbUmfc*3Ad^^sbjgj7rEHP`dQ2!|'
AUTH_SALT='&bsxRql:64W7]@b]Uy=vIE0WG7&AkjY`/=G)Fpv;yD+f9T;|_]Jh(ZidN^EyV]^t'
SECURE_AUTH_SALT='X/-Ms>!+dvRi#[email protected]{yGgU>X4nb3.Ur?kVXd#G(OBL[]@=eu$`i'
LOGGED_IN_SALT=']G0xBoAuE&.y5jp.`4OyKsJ9C`Bq>V1Ad&-&IT.[oHA?%`mxXRF{]V9+P.(A:>N['
NONCE_SALT='GZUU%*{WcGHPsQMU(8*<mZSyBA2e-6.wX2tAVh&+I7K]&8KU-U)bF:5aS(J:%1&G'

# Installing Plugins


Why configure plugins in Composer instead of giving users control?

On a managed WordPress project:


  • Users should not be able to update, delete, or add plugins which haven't been tested locally or on a testing server.

  • A plugin may work on its own, but in conjunction with other dependencies, there may be conflicts leading to errors and the famous 'blank screen'. Developers should first test for conflicts.

  • Anything that breaks on the site, even if it's because of the client, will need to be found and fixed by the developer.


Therefore, configuring plugins through Composer prevents potential issues. All a user can can do, is activate or deactivate plugins. We can thus make decisions about which plugins should be used.


WPackagist

We can add WordPress plugins that feature on the WordPress website https://en-gb.wordpress.org/plugins using Composer. But we have to do it through the a different repository called WPackagist. It features features both plugins and themes.

The instructions on WPackagist are simple, but I'll feature them here:



# First, tell Composer to look for packages in WPackagist

> composer.json
{
  "repositories":[
        {
            "type":"composer",
            "url":"https://wpackagist.org"
        }
    ]
}

Repository type

Normally Composer will look within the default Packagist repository. However, Composer allows us to define custom repositories, even using Git projects or zip files.



# Set the custom install path for the plugin's directory

> composer.json
{
  "extra": {
    "installer-paths": {
      "public/wp-content/plugins/{$name}": ["type:wordpress-plugin"]
    }
  }
}

The extra block

This contains custom installer-paths configurations. A plugin with a Composer package type of wordpress-plugin would be identified by Composer, and installed into the directory defined with its project name using the {$name} identifier.



# Configure a plugin and its version

In WPackagist, you can search for packages. Clicking on the version number gives you the exact line you need to add to the require block of the Composer file.

enter image description here



Example: installing the Yoast plugin

Here's an example with the WordPress SEO (aka Yoast) plugin.

> composer.json
"require": {
    "johnpbloch/wordpress": "~4",
    "wpackagist-plugin/wordpress-seo": "7.*"
}

By changing the version to 7.*, it will install the latest within version 7 for that plugin, rather than the specific version.



Run a composer update

$ composer update



Our folder structure should now look like this:

root
  |-- composer.json
  |-- composer.lock
  |-- public
    |-- wordpress
    |-- wp-content
      |-- plugins
        |-- wordpress-seo   # The installed plugin
      |-- themes
      |-- uploads
  |-- vendor

# Installing WordPress Themes


This is similar to how we install WordPress plugins. We still use the WPackagist repository that we defined before, however, we need to configure a new custom path for our new theme location:

> composer.json
{
  "extra": {
    "installer-paths": {
      "public/wp-content/plugins/{$name}": ["type:wordpress-plugin"],
      "public/wp-content/themes/{$name}": ["type:wordpress-theme"]
    }
  }
}



Example: installing the TwentySeventeen theme

This time, we'll install the default WordPress theme for 2017.

You can create the Composer configuration option by clicking the version you want:

WPackagist Theme Selection


> composer.json
{
  "require": {
      "johnpbloch/wordpress": "~4",
      "wpackagist-plugin/wordpress-seo": "7.*",
      "wpackagist-theme/twentyseventeen": "1.4"
  }
}



Then run composer update again.

$ composer update

# Keeping The Repository Clean

Commiting the WordPress core files, the vendor files or any other Composer installed packages in the project isn't necessary with Composer. These would amount to 1000s of files that are configured in the composer.json and are automatically added to the project with the composer install command.

Doing this in Git this is simple:

> .gitignore
vendor/
wordpress/
public/wp-content/plugins/
public/wp-content/themes/
public/wp-content/uploads/
.env
*.log

This tells Git to ignore the files in the vendor and wordpress folders etc. That means, our repository stays nice and clean, and low in size.

# Setting Up The Local Server


If you already have WAMP, MAMP or some other cool tool to create your local PHP server, feel free to continue. However, I highly recommend a virtual development environment using Vagrant or even a containerised operating system using Docker.

That's beyond the scope of this guide, but you can search online how to setup Vagrant or Docker.

# Summary


Working with WordPress in this way is not new. Scott Walkinshaw talked about his WordPress Composer solution called Bedrock in 2013. I learned of this in 2016 from Chris Sherry's via Rob Waller.

It has completely transformed how I work with WordPress. I started out with a haphazard development workflow commiting entire WordPress core files to version control. Imagine code reviewing hundreds of changed files that have nothing to do with a project when WordPress is updated.

I then discovered Pantheon which solved Git flow issues but it came with WordPress pre-managed. Being able to spin up an AWS instance or droplet, clone a repository, and composer install WordPress as a project dependency is easy to do, and I hope this guide has been easy for you to follow.

Extra Notes

  • Making our projects portable
    • by configuring dependencies
  • Good folder structure
  • Stopping users from changing the codebase on production
@roger-castaneda
Copy link

nice configuration but I'm wondering how to load some custom and unzipped paid plugins that I have in other folder like "paid/elementor-pro/". Would you please share an approach for that case? Thank you!

@przemekciacka
Copy link

@roger-castaneda if I were you I would put the Elementor plugin into Must Use Plugins folder (wp-content/mu-plugins). These plugins are automatically enabled and can't be uninstalled via WordPress Admin dashboard. You can read more about them at https://developer.wordpress.org/advanced-administration/plugins/mu-plugins/

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment