Last updated May 16, 2019

Managing a Drupal application with Composer requires a few modifications to Composer's default behavior. For instance, Drupal expects that specialized packages called "modules" be downloaded to modules/contrib rather than Composer's default vendor directory.

Additionally, it is common practice in the Drupal community to modify contributed projects with patches from Drupal.org. How do we incorporate Drupal-specific practices like these into a Composer workflow?

In this tutorial we will:

  • Address all of the Drupal-specific configuration necessary to manage a Drupal application using Composer

By the end of this tutorial you should know how to configure Composer to work with Drupal, and drupal.org.

Goal

  • Learn about the special Composer packages and configuration needed to build a Drupal application with Composer.

Prerequisites

Issues with Drupal's standard directory structure

The standard directory structure for a Drupal application is unusual. Drupal-specific quirks, sometimes called Drupalisms, pose a number of obstacles when using a standard Composer workflow.

Let's look at a few common examples. A typical Drupal application looks something like this (abbreviated) directory structure:

.git
.gitignore
docroot
+-- core
|   +-- libraries
|       +-- jquery.cycle
+-- modules
|   +-- contrib
|       +-- ctools
|   +-- custom
|       +-- my-module
+-- themes
|   +-- contrib
|   +-- custom
|       +-- my-theme
+-- profiles
|   +-- contrib
|   +-- custom
+-- sites
|   +-- default
|       +-- default.settings.php
|       +-- default.services.yml
|   +-- development.services.yml
|   +-- example.settings.local.php
|   +-- example.sites.php
+-- .htaccess
+-- index.php
+-- robots.txt
+-- ...
vendor
composer.json
...

Yikes! There are a few strange things going on:

  • There are contributed packages not listed on packagist.org
  • There are contributed packages placed outside of the vendor directory
  • Custom files sit side-by-side with contributed files in multiple directories.
  • There are some files provided by Drupal core, like index.php, that aren't actually in the core directory.
  • There are some files, like .htaccess, which are initially provided by Drupal core but are intended to be modified.
  • There are contributed packages written in both PHP and JavaScript

Questions to consider when updating

These Drupalisms pose a few problems when we attempt to update Drupal core and/or contributed modules. For instance:

  • How do we use Composer to download Drupal projects (modules, themes, etc.) if they aren't listed on Packagist?
  • How do we tell Drupal to put modules into modules/contrib but put themes in themes/contrib?
  • How can we use Composer to update core files like index.php, when those files aren't neatly separated into a dedicated core directory?
  • What if we've modified our .htaccess file, but we need to pull in an upstream update to .htaccess provided by a new version of Drupal core? How do we avoid wiping out our customizations?
  • What if we've modified our robots.txt and we don't want it to be updated by Composer at all?
  • What if we've patched a contributed module? How can we update in without wiping out the patch's changes?
  • How can we download a Javascript library?
  • How can we define Composer dependencies for a custom module?

We're going to tackle these problems one at a time. At the end, we will also provide solutions that address all of these issues in one fell swoop.

How Drupal is organized via Composer

To make these problems solvable, the Drupal core maintainers took the whole Drupal core project on Drupal.org and separated it into two Composer packages (available on Packagist): drupal/drupal and drupal/core.

Essentially, drupal/core is everything in the core directory, and drupal/drupal is a wrapper that is one directory above core and includes core, too.

Please note that the drupal/drupal project is a work-in-progress and not recommended for use. The Composer Support in Core Initiative is scheduled to release the in-progress work in Drupal 8.7.0, at which time drupal/drupal will become a viable option.

Composer-managed projects should require drupal/core and not drupal/drupal. This division allows us to neatly update the core directory in isolation by requiring and updating drupal/core. However, this solution alone does not let us easily update core files that live outside the core directory, like .htaccess, sites/default/default.settings.php, etc. These outliers are known as scaffold files.

Scaffold Files

How can we use Composer to update core files like index.php, when those files aren't neatly separated into a dedicated core directory?

To address the need of updating scaffold files, the Drupal Composer team created the Drupal Scaffold Composer plugin. A Composer plugin is a type of Composer package that can hook into the Composer API to perform specialized operations in response to Composer events.

You can install the drupal-composer/drupal-scaffold plugin for your Drupal application via:

composer require drupal-composer/drupal-scaffold

Once installed for your application, Drupal Scaffold will respond to any update to the drupal/core package. When drupal/core changes version, Drupal Scaffold will download and correctly place Drupal core scaffold files.

This behavior is configurable. You may configure Drupal Scaffold to do this for only some or for all scaffold files. See the Drupal Scaffold documentation for more information.

Downloading packages from Drupal.org

How do we use Composer to download Drupal projects (modules, themes, etc.) if they aren't listed on Packagist?

By default, Composer will search the Packagist repository for the packages defined in your composer.json file. Packagist does have a listing for Drupal Core. However, most Drupal projects (modules, themes, etc.) are listed on Drupal.org and not on Packagist.

In order for Composer to discover projects listed on Drupal.org, Composer needs a list of all packages that are available on Drupal.org. Drupal.org provides this information via a specialized Composer repository endpoint, available at https://packages.drupal.org/8.

Note that the "8" in the URL denotes that all packages returned by this endpoint will be for Drupal 8. This enables us to drop the major version digit from our Composer version constraints for Drupal packages. E.g., we may use "1.0.0" to download "8.x-1.0" of a Drupal package because the "8" is implied by our use of the https://packages.drupal.org/8 endpoint.

To make Composer aware of this endpoint, execute:

composer config repositories.drupal composer https://packages.drupal.org/8

That will add the following information to your composer.json file:

{ 
    "repositories": { 
        "drupal": {
            "type": "composer",
            "url": "https://packages.drupal.org/8" 
        }
    }
} 

To require a Drupal package, simply use the prefix drupal/ followed by the machine name of the Drupal project. For example, composer require drupal/ctools.

Note: You can use Composer to manage Drupal 7 sites as well, but this tutorial provides examples for the latest version of Drupal, which is 8.

Placing Drupal packages correctly

How do we tell Drupal to put modules into modules/contrib but put themes in themes/contrib?

By default, Composer downloads packages to the vendor directory. But Drupal requires that some packages be downloaded to a different location. For instance, contributed modules must be downloaded to modules/ in order for Drupal to find them. (And you may also want to place contributed modules in modules/contrib.)

This is a common issue amongst PHP frameworks that rely on Composer. To solve this problem, the Composer team has created the Composer Installers plugin. The plugin defines new types of Composer packages. Each package type has a corresponding default installation location. Composer Installers includes the following Drupal package types:

Package type Installation path
drupal-core core/
drupal-module modules/{$name}/
drupal-theme themes/{$name}/
drupal-library libraries/{$name}/
drupal-profile profiles/{$name}/
drupal-drush drush/{$name}/
drupal-custom-theme themes/custom/{$name}/
drupal-custom-module modules/custom/{$name}/
drupal-custom-profile profiles/custom/{$name}/

You can install the composer/installers plugin for your Drupal application via:

composer require composer/installers

Each Drupal project on Drupal.org should have a composer.json file that correctly defines the package type. When Composer downloads the package, the Composer Installers plugin (if installed) will inspect the type and place the package in the correct corresponding directory.

The directory mappings are configurable. For instance, if you keep your Drupal docroot in a subdirectory named docroot (below your composer.json file) you may define the following configuration in your composer.json file to correctly download modules:

{
    "extra": {
        "installer-paths": {
            "docroot/core": ["type:drupal-core"],
            "docroot/modules/contrib/{$name}": ["type:drupal-module"],
            "docroot/modules/custom/{$name}": ["type:drupal-custom-module"],
            "docroot/profiles/contrib/{$name}": ["type:drupal-profile"],
            "docroot/profiles/custom/{$name}": ["type:drupal-custom-profile"],
            "docroot/themes/contrib/{$name}": ["type:drupal-theme"],
            "docroot/themes/custom/{$name}": ["type:drupal-custom-theme"],
            "docroot/libraries/{$name}": ["type:drupal-library", "type:bower-asset", "type:npm-asset"]
        }
    }
}

One-off packages

This covers common Drupal packages well, but what if you have a "one-off" package that needs to be downloaded to a non-standard location?

The Composer Installers Extender plugin will allow you to arbitrarily place specific packages wherever you would like. For instance, it would allow you to download my/package to special/package/dir using the following configuration:

{
  "extra": {
    "installer-paths": {
      "special/package/dir/": ["my/package"]
    }
  }
}

See the The Composer Installers Extender documentation for more information.

Don't commit contributed packages

In Anatomy of a Composer Project, we learned that we should not commit vendor to our git repository. Drupal places core, modules, themes, and other contributed packages outside of the vendor directory. These packages should still be excluded from our Git repository, for the same reasons! You should use a .gitignore file that looks something like this:

docroot/core
docroot/modules/contrib
docroot/themes/contrib
docroot/profiles/contrib
docroot/libraries
vendor

Patching core and contributed packages

What if we've patched a contributed module? How can we update it without wiping out the patch's changes?

"Don't hack core" has long been the mantra of the Drupal community. But the fact is that sometimes a bug in core or a contributed module does require "hacking" code that doesn't belong to you. Perhaps a better mantra would be "don't hack core, but when you do, use a patch."

Patches are the accepted method for tracking and applying changes to Drupal core and contributed projects. Luckily, the Composer Patches plugin makes Composer an ideal tool for tracking, downloading, and applying these patches.

Since we do not commit contributed modules, patches will need to be applied by Composer each time that a package is installed or updated. This is the magic that makes hacking core with patches maintainable. Since contributed modules are not committed, we know that no developer working on our application can make a change to core code without documenting that change via a patch. Once the change is in a patch, it is applied in a consistent and reproducible manner.

To define a patch for a module, you must specify the patch information in your composer.json:

{
  "extra": {
    "patches": {
      "drupal/core": {
        "Clear Twig caches on deploys": "https://www.drupal.org/files/issues/2752961-90.patch"
      }
    }
  }
}

You may also use the Composer Patches plugin to apply locally stored patches, or to ignore patches defined by your dependencies. See Composer Patches documentation for more information.

Handling front-end dependencies

How can we download a JavaScript library?

Many themes and modules require the installation of front-end dependencies like JavaScript libraries. Composer is designed to exclusively install PHP dependencies, but there are a variety of ways to make Composer download non-PHP dependencies.

The implementation of these methods requires a measure of judgment. If you need to install only a handful of non-PHP libraries to make your PHP dependencies work, it's worthwhile to use Composer and avoid introducing a separate front-end dependency manager for your Drupal application. On the other hand, if your Drupal application requires many JavaScript libraries to work together, you should probably use a tool like NPM to manage those dependencies separately from your PHP dependencies.

Asset packagist

The Asset Packagist Composer repository provides information about Bower and NPM packages to Composer. When you run Composer commands for your project with Asset Packagist, Composer knows all the available releases of Bower and NPM packages and it knows how to download their files.

To make Composer aware of Asset Packagist, execute:

composer config repositories.assets-packagist composer https://asset-packagist.org

That will add the following information to your composer.json file:

{ 
    "repositories": { 
        "asset-packagist": {
            "type": "composer",
            "url": "https://asset-packagist.org" 
        }
    }
} 

By default, this will install front-end packages like bower-asset/bootstrap to the vendor directory. However, you can use the Composer Installers Extender plugin (documented above) to place your front end libraries in a custom location. For instance:

{
    "require": {
        "oomphinc/composer-installers-extender": "^1.1",
        "bower-asset/bootstrap": "^3.3",
        "npm-asset/jquery": "^2.2"
    },
    "extra": {
        "installer-types": ["bower-asset", "npm-asset"],
        "installer-paths": {
            "public/assets/{$vendor}/{$name}/": ["type:bower-asset", "type:npm-asset"]
        }
    }
}

The above configuration will cause bower-asset/bootstrap and npm-asset/jquery to be downloaded to public/assets/.

See Asset Packagist documentation for more information.

Custom packages

How can we define Composer dependencies for a custom module?

Most Drupal applications include at least one custom module or theme. You can easily define Drupal dependencies in a *.info.yml file, but what about non-Drupal dependencies?

Let's say you have the following files:

modules/custom/my-module/composer.json
composer.json

If you execute composer install in your application's root directory, Composer will (rightly) only care about the composer.json file in your root directory. It will completely ignore modules/custom/my-module/composer.json.

The simplest solution to this is to define all dependencies in the root composer.json, but this isn't preferable for code organization. Another option is to use the Composer Merge plugin.

Wikimedia's Composer Merge plugin will merge multiple composer.json files at Composer runtime. This plugin is actually used by Drupal itself to merge drupal/core dependencies into drupal/drupal.

In our example, you can use the plugin to effectively merge docroot/modules/custom/my-module/composer.json into composer.json during the execution of a composer install or composer update command.

First, require the plugin via:

composer require wikimedia/composer-merge-plugin

Then, add the following configuration to your root composer.json:

{
    "extra": {
        "merge-plugin": {
            "require": [
                "docroot/modules/custom/my-module/composer.json"
            ],
            "replace": false,
            "ignore-duplicates": true
        }
    }
}

The behavior of this plugin is configurable. See the Composer Merge Plugin documentation for more information.

Recap

In this tutorial, we examined the structure of a typical Drupal application and considered the challenges that Drupalisms pose for Composer-managed applications. For each challenge, we looked at one or more possible solutions.

Below is a sample composer.json file that incorporates all of the suggested solutions:

{
    "name": "me/my-project",
    "license": "proprietary",
    "type": "project",
    "repositories": {
        "drupal": {
            "type": "composer",
            "url": "https://packages.drupal.org/8"
        },
        "asset-packagist": {
            "type": "composer",
            "url": "https://asset-packagist.org"
        }
    },
    "require": {
        "composer/installers": "^1.2.0",
        "cweagans/composer-patches": "^1.6.4",
        "oomphinc/composer-installers-extender": "^1.1",
        "wikimedia/composer-merge-plugin": "^1.4.1"
    },
    "require-dev": {},
    "extra": {
        "enable-patching": true,
        "installer-types": ["bower-asset", "npm-asset"],
        "installer-paths": {
            "docroot/core": ["type:drupal-core"],
            "docroot/modules/contrib/{$name}": ["type:drupal-module"],
            "docroot/modules/custom/{$name}": ["type:drupal-custom-module"],
            "docroot/profiles/contrib/{$name}": ["type:drupal-profile"],
            "docroot/profiles/custom/{$name}": ["type:drupal-custom-profile"],
            "docroot/themes/contrib/{$name}": ["type:drupal-theme"],
            "docroot/themes/custom/{$name}": ["type:drupal-custom-theme"],
            "docroot/libraries/{$name}": ["type:drupal-library", "type:bower-asset", "type:npm-asset"],
            "drush/contrib/{$name}": ["type:drupal-drush"],
            "special/package/dir/": ["my/package"]
        },
        "merge-plugin": {
            "require": [
                "docroot/modules/custom/my-module/composer.json"
            ],
            "replace": false,
            "ignore-duplicates": true
        },
        "patches": {
          "drupal/core": {
            "Clear Twig caches on deploys": "https://www.drupal.org/files/issues/2752961-90.patch"
          }
        }
    },
    "minimum-stability": "dev",
    "prefer-stable": true
}

In the next section, we will look at ways to quickly and easily create a new Drupal application that contains much of this configuration out of the box.

Further your understanding

  • What is the difference between drupal/drupal and drupal/core?
  • How can you use Composer to download modules to docroot/modules/contrib?
  • How can you use Composer to update .htaccess?
  • When shouldn't you use Composer to manage frontend dependencies?

Additional resources