Drupal and Composer: Part 4 — Composer for Drupal Developers
As any developer working with Drupal 8 knows, working with Composer has become an integral part of working with Drupal. This can be daunting for those without previous experience working with command line, and can still be a confusing experience for those who do.
This is the fourth post in an explorative series of blog posts on Drupal and Composer, hopefully clearing up some of the confusion. The four blog posts on this topic will be as follows:
- Part 1: Understanding Composer
- Part 2: Managing a Drupal 8 site with Composer
- Part 3: Converting Management of an Existing Drupal 8 Site to Composer
- Part 4: Composer for Drupal Developers
If you have not yet read part 1 and part 2, then before reading through this post, it is probably best to ensure you understand the concepts outlined in the summaries of those articles before moving forward.
Composer for Drupal Developers
The final section in this series addresses how Drupal developers can integrate profiles, modules and themes with Composer, to manage 3rd party libraries and/or other Drupal modules used by their code. To understand when and why a developer would wish to do this, let's have a look at a use case.
Use Case 1: Instagram Integration
Imagine a developer building a module to integrate with Instagram, pulling the most recent images from an Instagram account and displaying them on the Drupal site. Instagram provides an API for retrieving this information, but before Instagram provides this data, the Drupal site is required to authenticate itself to Instagram, using the OAuth2 protocol. After authenticating, various API calls can be made to retrieve data from the API using the the OAuth2 authentication token.
Up to Drupal 7, the Drupal way to do this would be to write a custom OAuth 2 integration (or use the OAuth2 Client module), then write custom functions that made the API calls to Instagram, retrieving the data.
Drupal 8 however, using Composer, acts like a hub, allowing for the inclusion of 3rd party libraries. This saves developers from having to re-create code within Drupal (aka rebuilding the wheel). For example, Instagram integration can be handled with the Instagram-PHP-API library. This library provides OAuth2 integration specific to Instagram, as well as various PHP wrappers that interact with Instagram's API to handle the retrieval of data from the API. Using Composer, our developer can integrate this 3rd party library into Drupal, and use it in their Drupal code. This saves our developer the time and effort of creating and testing the code that integrates with Instagram, as these tasks are handled by the library maintainers. If the library is well maintained, it will be updated in line with updates to Instagram's API, saving developer time (and therefore client money) in maintenance as well. And finally, as the library is open source, it will have the benefit of having eyes on it by PHP developers from various walks, rather than just Drupal developers, making for a stronger, more secure library.
Use Case 2: Drupal Module Dependency
Imagine that it is a requirement that the images retrieved from Instagram be shown in a popup (aka modal or lightbox). This can be handled using the Drupal Colorbox module. As such, the Colorbox module will be set as a dependency in the Instagram module's
.info.yml file. When a site builder managing their Drupal project with Composer downloads/manages our developer's Instagram module with Composer, then tries to enable the Instagram module in Drupal, they will get an error from Drupal that the Colorbox module is missing. The problem here is that Drupal is being told the Colorbox module is a dependency, but we are not managing that dependency with Composer, and therefore the code has not been downloaded and does not exist.
At this point, it is easy to think that site builders could just add the Colorbox module to Composer with a
require command. This would work, and after doing so, they would be able to enable the Instagram module, since the Colorbox module now exists. This opens up a problem however; imagine the site builder later decides to remove our developer's Instagram module. They disable/uninstall it in Drupal, then use
composer remove to remove it. Everything looks good - the module has been uninstalled from Drupal, and the code has been removed from the codebase. However, there is one more step to return the system to its original state; disabling/uninstalling the now unnecessary Colorbox module from Drupal and removing the code using Composer. The necessity of this additional step opens up situations where the module will be left on the system due to forgetfulness, a lack of documentation, or changing site builders, creating site bloat due to unnecessary code existing and being executed.
The solution to this is to also manage Drupal profile/module/theme dependencies with Composer. In our use case, our developer will list the Colorbox module as a dependency of the Instagram module not just in the Instagram module's
.info.yml file, but also as a Composer dependency in
composer.json. This way, when the Instagram module is added to a project using Composer, Composer will also download and manage the Colorbox module. And when the site builder removes the Instagram module, Composer will remove the Colorbox module as well (if no other libraries list it as a dependency).
Integrating Drupal Code with Composer
Step 1: Create a composer.json file
Integrating with Composer requires that a
composer.json file be created in the root of the profile/module/theme (from here on out referred to as the Drupal library) folder. This is explained in detail on the Drupal.org documentation page Add a composer.json file. Once this file has been added to the Drupal Library, it should be validated. This can be done by running
composer validate on the command line in the same directory that the
composer.json file lives in.
Note that Drupal.org automatically adds a
composer.json file to projects that do not have one. This is so that Composer is able to manage all projects on Drupal.org, not just projects to which maintainers have explicitly added a
Step 2: Declare your dependencies to Composer
All library dependencies, whether 3rd party libraries, or contributed Drupal libraries (modules etc), need to be declared as dependencies to Composer. This is done by calling
composer require [LIBRARY NAME], from within the folder of the Drupal library. This means that if you are adding dependencies for a module, you will navigate to the module folder, and add your dependencies. If you are adding dependencies for a theme, navigate to that folder, and add the dependencies there. The key point here is to not add your dependencies from the wrong folder, as they will be be added to the
composer.json file of the wrong package.
Adding Remote Library Dependencies
In the use case example for this article, the Instagram-PHP-API library was suggested for integration with Instagram. This library has the Composer key
cosenary/instagram. To set this library as a dependency of a module, navigate to the root of the module, and run the following:
composer require cosenary/instagram
Running this code results in the following:
- The cosenary/instagram library is added as a dependency to the
composer.jsonfile, so that Composer knows the package is managed by Composer. The
composer.jsonfile will contain something like the following:
Note that composer.json is committed to Git.
The [MODULE ROOT]/vendor folder is created, and the Instagram-PHP-API library is downloaded into this folder. Composer by default creates the
vendorfolder in the same directory as the
Note that once the Drupal library has been pushed to a repository where it can be managed by Composer, installing the Drupal library with Composer will install the Instagram-PHP-API library to the project's vendor folder. As such, it's a good idea to delete the vendor folder in the module after the new
composer.lockfiles have been committed to the remote repository, before requiring the module with Composer.
As a best practice, it is a good idea to create a
.gitignorefile in the module root, and include the vendor directory to ensure that it is never accidentally committed.
composer.lockfile is created, locking the installed version of the Instagram-PHP-API library to the Instagram module. This ensures that users of the Instagram library are working with a compatible version of the Instagram-PHP-API library. Note that this file is also committed to Git.
Any dependencies of the Instagram-PHP-API library, declared in its own
composer.jsonfile, are downloaded.
All libraries and dependencies are checked for version conflicts.
Adding Drupal Library Dependencies
In the use case example for the article, the Drupal Colorbox module (aka library) was mentioned to be a dependency of the Instagram module, and therefore is to be managed with Composer. Adding Drupal modules as dependencies requires an additional step, due to their being hosted on a non-default repository. To add a Drupal library as a dependency:
- Edit the
composer.jsonfile and add the Drupal repository to it, so that Composer knows where to look for the Drupal libraries, including the Colorbox module:
Now Composer knows where to look for the Colorbox module when the Instagram module is installed. If developers try to declare Colorbox module as a dependency before this step, they will get an error that Composer cannot find the
- Add the module with composer require:
composer require drupal/colorbox
Note that this will download the Colorbox module to
[MODULE ROOT]/vendor/drupal/colorbox. This is NOT a good thing, for while Drupal will be able to find the module in this location, it is not the standard location, and if another copy of the module ends up in other directories that Drupal scans when looking for modules this will create hard to debug issues. So either delete the Colorbox module in the vendor folder right away, or immediately move it to the location where the rest of your contributed modules are located, so it's very clear where it is at.
After this, both the
composer.json file and the the
composer.lock file should be committed to Git. The module now has its dependencies declared on both the Instagram-PHP-API library and the Drupal Colorbox module. When site builders install the module to their Drupal project using Composer, the Instagram library will be downloaded to the project (site)
vendor folder, and the Drupal Colorbox module will be installed to the
Developing With Composer-Managed Drupal Libraries
Now that the module has been integrated with Composer, Composer can be used to manage the module during development. Let's imagine that the Instagram module being developed has the key
drupal/insta, and is on Version 8.x-1.x. Downloading the -dev version of the module can be done by appending
:1.x-dev when requiring the module:
composer require drupal/insta:1.x-dev
This will download the 8.x-1.x-dev version of module to the
web/modules/contrib folder, where it can be worked with. A .git folder will be included in the module root, so that changes can be committed to Git. Sounds great, but when our developer tries to push their commits, they will get an error, as the remote URL of the repository is not the repository for maintainers of the module and does not allow commits. As such, we need to change the Git repository's remote URL to the URL of the repository for maintainers, allowing for code to be pushed, and releases to be made.
To find the correct Git repository URL, first go to the module's download page on Drupal.org. Make sure you are logged in as a maintainer of the module. If you are a maintainer and have the correct permissions, you will see a tab called version control. Click this tab. Next, copy the Git URL on the page. It will look something like this:
Next, navigate to your module folder, and run the following commands:
git remote rm origin[MODULE NAME]
git remote add firstname.lastname@example.org:project/
The first line of this code removes the incorrect remote Git URL for non-maintainers, and the second line adds the correct Git remote URL for maintainers. After this, you will be able to push changes, including tags (releases).
You can also then easily switch between your development and release versions of the module by alternating the following:
composer require drupal/insta
composer require drupal/insta:1.x-dev
Note however that when doing any commits, you will need to switch up the remote URL each time you switch to the dev version of the module.
In this final part of the series on using Composer with Drupal, we have looked at how Drupal developers can integrate their custom code with Composer, to ensure that dependencies of the module are correctly managed. We have also looked at how to develop modules that are being managed with Composer. Using these techniques will allow site builders to keep a clean codebase that manages library conflicts.
And this concludes my four-part series on Drupal and Composer. I hope you have enjoyed it, and that it shows just how powerful Composer is, and how it can be effectively used to manage Drupal 8 sites. Happy Composing and Happy Drupaling!