WordPress + Composer + GIT

Matías Halles
Aug 11, 2016 · 14 min read

A tutorial for a tearless and fearless journey to WP nirvana.

This tutorial comes out of a small presentation done at The Auckland WordPress users group. Here are the slides. Lot’s of emoji. Thanks so much for receiving me :)

The usual WordPress workflow includes using code editing in a local environment, then using an FTP client (like Cyberduck) to upload your files and maybe change content or configurations through your admin panel on the site online. This works great (not so great) when working by yourself in a project.

“I’m a strong independent developer who don’t need no team.” — No one, ever.

So what happens if you want to work with someone else on a WP project? Same thing, code, upload through FTP, go into admin maybe, refresh browser. Unless your team mate just added some code in the same file you were editing, or changed a function that you were relying on, stepping on your code and leaving you with a missing piece of code. You’ll probably refresh and see some weird error going on in the site. Hopefully not on your already live client’s website. Hopefully not a php fatal error. Fixing the site while getting calls from your client = nope 😨

For those who know what git, composer, WordPress Packagist, ssh, ftp, blah, bleh, derp and hurr are, just jump onto the tutorial :)

Git for Version Control

Git allows you to track historically the changes in your code, being this your themes, your plugins or anything that has plain text. Git can track binary files, but it is not very good at it. You will have problems at some point and getting rid of these problems is not easy. I advice you not to unless they are small files that can’t be sourced in some other way.

Also one of Git’s objectives is to allow for distributed teamwork. It has tools that allow every dev worked separately and freely, without interrupting each other, and then merging changes in an orderly manner.

Advantages of using Git

  • Work with a team

Composer + WPackagist

Composer is a dependencies managing tool for PHP. A dependency manager is a tool that will let you configure what your software depends on to work and make these dependencies available for you with no hassle. Think of it as a cook assistant: For bread you need 1 Kg of flour, 3 cups of water, 1 cup of milk, 100 gr of butter, 1 teaspoon of salt. Make your assistant get the stuff and have it ready for you to focus on cooking and not going grocery shopping. You can install composer directly into a project but I recommend you to install it system wide.

WordPress Packagist is a repositories site that automatically packages the plugins and themes available in WordPress site in the format required for working with composer. This is your groceries store.


  • Control used versions of WordPress, plugins and themes, preventing incompatibilities.

SSH and use the speed you deserve.

FTP man. It is so painfully, ridiculously, sloth, slow. Every. Damn. Time.

There’s a bunch of technical reasons why FTP uploading is slow, which don’t really matter. What matters is that login in to your server through SSH will let you stop uploading your files and start downloading them onto the server, at server speeds and directly from the original sources, being that WordPress servers, WordPress Packagist and others. In the tutorial below, you’ll be able to experience the speed difference.


  • Speed uploads, because no uploads.

Overall Advantages

  • No more code smashing


  • Learning Curve (not so much)

The Tutorial

This tutorial will show you the basics on how to make all these tools work to your benefit. I will be assuming you already have all of these tools set up and know a bit of they are about:

  • SourceTree: (git graphical client for OS X and Windows. Comes with git embedded). I’m sure you linux guys can figure out what client to use. I haven’t used linux on desktop for a long time sorry :p

If you want to run your WordPress install, you should have your server stack running. This tutorial will not focus on running WordPress but on the development process.

All set? Ok then :)

Create 2 new repositories and clone them into your computer.

It doesn’t really matter what names you give them. Just make them clear that one is a the site and another is for the theme. The tutorial will produce two repositories like these:

To create a new repository in github, click on the + sign on the top right corner of the interface. A dropdown will drop, choose “repository”. For the sake of simplicity, choose to add a README file and a license. Do the same for the theme repository:

Image for post
Image for post

Now, time to clone your site repository locally. Go into the site repository and find the “clone or download” button. A drop down will pop with a URL. Copy that address to use in SourceTree afterwards.

Now, with SourceTree open, clone the repository into a directory. It really doesn’t matter what “Name” you choose for the purpose of the tutorial.

Image for post
Image for post

SourceTree will open the project in a new window. Switch to the history tab on the left column to see the project’s history, which will have only one commit at this point.

Image for post
Image for post

Currently there’s just a README.md file and a LICENSE file. We’ll first add a .gitignore file (tasked to configure what to ignore in a repository) and then a composer.json file which will have our initial composer configuration.

Create a file called .gitignore on the base of the project with your favorite code editor (remember it starts with a .) and add these lines:


This four lines will prevent git to track and nag about files installed under /content/ and /wp/ (which is where our plugins, themes and WordPress core will end up, as well as our wp-config.php file which should never be tracked. .DS_Store is there just for mac users. Annoying little pricks, those files. Next step, go to SourceTree. You’ll see you have a new point in the history with the name “Uncommited Changes”. Check the empty checkbox at the left of the file called .gitignore, press “Commit” on the toolbar. An input box will show up underneath. Fill it up with a comment that describes what you just did so you have an idea of what went on during that change. Could be just “add gitignore” and the press “commit” on the lower right corner. For those unfamiliar to git, it is a very good practice to be clear on what happened in that point on history. Your repository in Sourcetree should look like this:

Image for post
Image for post

Now let’s do the same creating a file called composer.json. This will have all of our configuration for dependencies and other things like, where to put wordpress related modules and it’s core. The contents of the file should be:

"name": "halles/wp-composer-site",
"description": "Basic template for creating a composer + wpackagist controlled site",
"authors": [
"name": "Matias Halles",
"email": "matias.halles@gmail.com"
"require": {
"wordpress": "4.5.*",
"wpackagist-plugin/w3-total-cache": "0.9.2",
"wpackagist-plugin/google-analytics-for-wordpress": "5.5.2"
"require-dev": {
"wpackagist-plugin/debug-bar": "0.8.2",
"wpackagist-plugin/debug-bar-console": "0.3"
"type" : "package",
"package" : {
"name" : "wordpress",
"type" : "webroot",
"version" : "4.5.2",
"dist" : {
"url" : "https://github.com/WordPress/WordPress/archive/4.5.2.zip",
"type" : "zip"
"source" : {
"url" : "https://github.com/WordPress/WordPress",
"type" : "git",
"reference" : "4.5.2"
"require" : {
"fancyguy/webroot-installer" : "1.0.0"
"type": "composer",
"url": "https://languages.koodimonni.fi"
"autoload": {
"psr-0": {
"Acme": "src/"
"config" : {
"vendor-dir": "content/vendor"
"extra" : {
"installer-paths": {
"content/plugins/{$name}/": ["type:wordpress-plugin"],
"content/themes/{$name}/": ["type:wordpress-theme"]
"webroot-dir" : "wp",
"webroot-package" : "wordpress",
"wordpress-install-dir": "wp",
"dropin-paths": {
"content/languages/": ["vendor:koodimonni-language"],
"content/languages/plugins/": ["vendor:koodimonni-plugin-language"],
"content/languages/themes/": ["vendor:koodimonni-theme-language"]

Go back to Sourcetree and create a commit with this file. This will ensure we can then modify the file and see what will be changing through out the toturial. We’ll go over some of the properties defined in this file.

You should change the “name” property to mimic what your user and repository name are. The description and author or authors you can leave them empty. It’s up to you.

Now the require and require-dev properties list what your composer controlled site requires on production and development environments. By default development environment will install production environment as well (more on this later). Now we have the first reference to modules and WP core, specifying the any subversion of the 4.5 version of WP, and specific versions for each plugin. For the sake of the exercise, these are not the latest versions for the plugins. You can go into WordPress Packagist and lookup latest versions for each plugin.

Now let’s open up a Terminal on the repository’s path and let composer do it’s job:

composer install
Image for post
Image for post

Fire up your file browser and explore your newly installed files. You’ll find the WordPress core under ./wp/ and plugins and themes under ./content/. You can configure these paths in composer.json under the extra property.

Another important thing that happened when executing composer install, it’s that composer created a lock file called composer.lock, because it didn’t exist. This file explicitly declares the particular versions and commits available at the moment of creation. Whenever someone executes ‘composer install’, composer will use the contents of this file and not composer.json. To update composer.lock from composer.json you have to execute ‘composer update’.

This is because not always you’ll have a specific version in the .json file, but the only way to make sure a software works is based on the tested version. Let’s commit this composer.lock file to see how that would work.

Let’s modify the composer.json file. We’ll change only versions of some plugins and the wordpress core. The core version needs to be specified, but the versions on plugins can be left as “latest” using ‘*’ as the version:

Change these lines in the require and require-dev properties (from line 10:- "wpackagist-plugin/w3-total-cache": "0.9.2",
+ "wpackagist-plugin/w3-total-cache": "*",
- "wpackagist-plugin/debug-bar": "0.8.2",
+ "wpackagist-plugin/debug-bar": "*",
- "wpackagist-plugin/debug-bar-console": "0.3"
+ wpackagist-plugin/debug-bar-console": "*"
And for wordpress, replace all three appearances of 4.5.2 by 4.5.3 under wordpress package definition (from line 25).- "version" : "4.5.2",
+ "version" : "4.5.3",
- "url" : "https://github.com/WordPress/WordPress/archive/4.5.2.zip",
+ "url" : "https://github.com/WordPress/WordPress/archive/4.5.3.zip",
- "reference" : "4.5.2"
+ "reference" : "4.5.3"

After that, execute composer update. It will also execute composer install as part of the process and update the needed files:

Image for post
Image for post

Be sure to commit composer.json and composer.lock into a new commit to keep the changes. Now that you’ve seen how fast and controlled an update can be. Let’s pretend the update didn’t work, and go back to the previous commit. Right click on the previous commit and use checkout. It will ask for a confirmation. Go with it and press Ok.

Image for post
Image for post

After executing composer install (not composer update this time, because we already have a composer.lock there) all files will be rolled back to the previous version.

Image for post
Image for post

Let’s again checkout the latest commit and use composer to install the latest versions of out dependencies to keep on advancing.

Working with a theme through Composer

You’ll need to clone your theme repository inside the themes directory. This directory is not created yet, as no themes have been installed, so you’ll have to create it. The configure path (as it is in composer.json) will be ‘your-site-repo/content/themes/’. Be sure to create and clone it in the correct path.

Image for post
Image for post

You will have to create a composer.json file as well for this repository. As before, the name doesn’t have to resemble your repository name in github but it is recommended. That is the only important part as this will be the reference to integrate it into your site repository.

"name": "halles/wp-composer-theme",
"authors": [
"name": "Matias Halles",
"email": "matias.halles@gmail.com"
"type" : "wordpress-theme",
"require" : {
"composer/installers": "~1.0"

Now your repository should look like this:

Image for post
Image for post

Now the tricky part. You need to “push” the commit from the theme repository into the server to allow the site’s repository to use it. After we need to add the package information to out site’s composer file, and also instruct it to install it from that package:

Into the require property add:"halles/wp-composer-theme": "dev-master",And under repositories:{
"type": "git",
"url": "git@github.com:halles/wp-composer-theme.git"

Be aware that the version for “halles/wp-composer-theme” is “dev-master”, which translates to “last commit on master branch”. You can use tags and branches this way. Now your modified composer.json should look like this:

Image for post
Image for post

Now, you should be able to execute composer update and connect both repositories successfully. The output should look like this:

Image for post
Image for post

Now your new composer.lock file has been created. And it is ready for letting you work on your theme. Let’s commit this and let’s add WordPress valid stuff into the theme. I just shamelessly copied files from an old theme located on bitbucket. Made it into a few commits in the theme, pushed the commits, and then updated our composer.lock file in the site, twice. Also added a new plugin and updated composer.lock file. All for the sake examples. Take a look at the history at github on the example repositories.

Now, the whole process might feel a bit complicated. But once you get used to it, adds no over head and let’s you version control your site code base and share it across the team with never fearing you’ll loose code when sharing.

Now, what about deployment.

This is the fun part. Once you have both repositories working in sync, deployment and updating among team mates or across any server or environment.

If you are cloning a project, all you have to do is clone it, and then install with composer:

Image for post
Image for post
Image for post
Image for post

Now, all your files are downloaded, ready for usage by your server 🎉

About customized paths for WordPress

As you remember, we didn’t install WP under the root of the project, and replaced ‘wp-content’ by ‘content’. So this won’t work properly straight away yet. We need to make some special configurations:

  • A custom index.php file which will bootstrap WP on the new path

I will add a default wp-config-sample.php file in the repository with the directives. Remember we added wp-config.php to our .gitignore file? When downloading your site onto the server you will have to make a copy of this sample file named wp-config.php and edit it. The wp-config.php is not supposed to be saved or tracked by git, because it is the file that will hold sensitive information: db connection information and hash salts.

Out index.php file will look like this. Notice the only difference in this file when compared to the index.php in WordPress’ core files is the /wp/ we are adding to the path of the require directive:

define( 'WP_USE_THEMES', true );
require( './wp/wp-blog-header.php' );

And our wp-config-sample.php file looks like this:

* Basic WP Config
define('DB_NAME', 'db_name');
define('DB_USER', 'db_user');
define('DB_PASSWORD', 'db_pass');
define('DB_HOST', 'db_host');
define('DB_CHARSET', 'utf8mb4');
define('DB_COLLATE', '');
$table_prefix = 'wp_';# Rememeber to get your salts at http://api.wordpress.org/secret-key/1.1/salt/define('AUTH_KEY', 'y%*XT).2%GS9D(DDaPmaH|dg8)BMh*>$w+(S2vWH!=avSS>Q9sdLMn<$`sv<a/!Z');
define('SECURE_AUTH_KEY', 'Bs >->v>0%`&2{^o^OP9Ta|wu9ESalMU?Y-^*Dd?Q$Fn:d{F:TmBp=r?nrW$dX<#');
define('LOGGED_IN_KEY', ';V-M%Z3M[IoJ{q_73gE25+-M@ge}B80DhkSRvx:Ax,/5gB@!2IOH4fv@kjbD()T`');
define('NONCE_KEY', 'qwsK_l*Tx)>X%bLm2+:1z%8a6|.+Yz7S=T2 +m<1jD!P2,pQ=R5(3aYl76W&{=tU');
define('AUTH_SALT', '):>bXn.2rVQ=m-mx[|^b!-e LKE`__Lp0V;zC>bB+--</Pnh3@cX7-2f=`RXM:eS');
define('SECURE_AUTH_SALT', 'Juey96K7;<KDIB>-8egHPge<+!rtt>OnK&:G$/bb[G-OWdfWx4{y9Kk?ka(GK7rb');
define('LOGGED_IN_SALT', 'Bb*on-)LpeuPEul0<GTY}*+|aQ--a;-QK/$ad)8b&oY+TkS)<]= z+(UI]jP]4pm');
define('NONCE_SALT', 'q!eu@*@_P3=!ovElTnP^?/1-`-tfvFa;Y;n%SizxtM*O2->V|%-hV*=O!Zx m!wy');
define('WPLANG', EN_us);/**
* Custom WordPress Install Path
# Sets the site's admin location and the site's location, respectively
define( 'WP_SITEURL', 'https://yourhost.io/wp' );
define( 'WP_HOME', 'https://yourhost.io' );
# Sets the content location, related to what's defined on composer.json file
define( 'WP_CONTENT_DIR', dirname( __FILE__ ) . '/content' );
define( 'WP_CONTENT_URL', WP_HOME . '/content');
# Sets the plugins location, related to what's defined on composer.json file
define( 'WP_PLUGIN_URL', WP_CONTENT_URL . '/plugins' );
# Disables the embebeded editor
define( 'DISALLOW_FILE_EDIT', true);
define( 'DISALLOW_FILE_MODS', true);
define( 'RELOCATE', true);
# Disables automatic update functions
define( 'WP_AUTO_UPDATE_CORE', false );
* You might want to force SSL on the admin page
# define( 'FORCE_SSL_LOGIN', true );
# define( 'FORCE_SSL_ADMIN', true );
* Debug Flags
* Use them under development environments
define('WP_DEBUG', false);
define('WP_DEBUG_LOG', false);
define('WP_DEBUG_DISPLAY', false);
define('SAVEQUERIES', false);
/* KEEP OUT BELOW *//** WordPress 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');

And that’s all folks.

This we do at Wikot to share work among our dev teams in several countries in an efficient and orderly way. Also, this let’s us have a clean overview of whatever else we may have in the directory, as well as backup uploaded media in a clean way when regular storage is used. More important, it stops us from wanting to kill each other 😅

If you have any questions, you can find me easily on twitter by @halles :)

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store