You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
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.
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:
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:
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:
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
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.
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.
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
<?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.
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.
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:
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
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
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.
Example: installing the Yoast plugin
Here's an example with the WordPress SEO (aka Yoast) plugin.
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:
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.
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.
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.
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!
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!