Summary
Composer is a dependency management tool for PHP-based applications. Composer is PHP’s equivalent of node’s NPM or ruby’s gems.
Composer allows you to do several important things:
- Maintain a list of which external libraries are necessary to run your application
- Maintain a specific version for each of those external libraries in a way that does not require them to be under version control
- Maintain a list of platform requirements necessary to run your application
- Provide a way to autoload classes from your application and your external libraries
This article details some of the nuances to using composer, with a specific emphasis on using it for Magento 2 projects (however the concepts presented here are applicable to almost any php-based application).
Overview of a composer project
The file structure of a composer project usually looks like this:
app/
|__ your custom code
.git/
|__ source code
.gitignore
|__ gitignore
composer.json
|__ composer list of required packages
composer.lock
|__ list of specific versions used for the above packages
vendor/
|__ All external libraries. This should not be in git!!!
vendor/autoload.php
|__ The autoload.php file – this file must be manually require()’d in your application!!
Using Magento 2
The rest of my tutorial is going to be related to using composer with Magento 2. In this situation, composer.json is already setup. So, this tutorial is not going to cover how to start a composer project from scratch, nor will it cover how to package a module for use in other composer applications.
Installing Composer
Regardless of whether or not a project uses composer.json, you must have the composer.phar file installed on your server.
Initial Installation
When you first download a Magento 2 instance, you’ll want to run:
composer install
So what’s happening with this command? Well, a few things:
- The install command is a signal to the composer.phar file that we want to install all the dependencies listed in composer.lock
Notice that I said composer.lock not composer.json. We’ll go into this more later, but composer.lock has the specific package versions we want.
- Each external package is downloaded at the appropriate version, and installed inside the vendor folder. You’ll notice that this folder gets created if it doesn’t exist.
Repository Locations
Most external packages are public open-source packages. These are hosted on packagist.org and it’s the default location that composer looks for external libraries.
In some instances, the package creator will wish to limit access to who can download their code. There are a couple ways of handling this:
1. Private or Public repositories – this is probably the most simple mechanism. You will be granted access to a private git repository (or shared a public git repository link), and you must configure this repository under the repositories node in composer.json.
2. Authenticated repositories – similar to the above but slightly different. Essentially, you register a private repository location in the repositories node, but you then supply authentication credentials for that repository in auth.json – this is the mechanism generally used for packages that will have a wide distribution.
Below is an example
"repositories": {
"0": {
"type": "composer",
"url": "https://repo.magento.com/"
},
"acme-fraud": {
"type": "git",
"url": "https://github.com/Acme/fraud.git"
}
},
Package Format
If you look inside the vendor/ folder you’ll see a common format for all composer packages:
VENDOR_NAME/
|______ MODULE_NAME/
|__________ composer.json
|__________ autoload.php
Composer allows each package to define its own autoloading methodology, its own additional dependencies (in the composer.json file) and its raw source code (most packages put this in the src/ folder).
Customizing the installation
Now we’re going to get into the challenging part – adding, updating, and removing packages.
Adding New Packages
Adding a new package is pretty easy, and looks like the below:
composer require VENDOR_NAME/MODULE_NAME
This will install the given module at the latest stable version. You can tweak the command to use specific versions if necessary
composer require VENDOR_NAME/MODULE_NAME “VERSION”
See this link for all possible version operators. It’s possible to install based on a number of things:
- Install a specific version
- Install a version >= a minimum version
- Install a version <= a maximum version
- Install a version >= a minimum AND <= a maximum
- Install the latest stable version >= a minimum version BUT LESS THAN the next major release
https://scotch.io/tutorials/a-beginners-guide-to-composer
composer require VENDOR_NAME/MODULE_NAME --no-update
This command will install the given module, but not any of its dependencies.
composer require vendor_one/module_one vendor_two/module_two
This can be used to require multiple packages at once.
It’s worth noting that because you don’t have to pass a version to require (or you can just pass it a “version range”), you may see different behavior than someone else on the team. Composer maintains a cache of repositories that it updates from time-to-time. So, it’s possible you have an earlier version of a package cached than someone else.
The same is true for what happens over time. 6 months later, requiring the same package will likely result in a different version (for both the package and its dependencies).
What happens when a new package is added?
So, when you add a new package, several things happen:
- An entry is added to composer.json specifying that a new package was installed, and gives a specific version constraint (or, if no version constraint was provided, a * is used)
- An entry is added to composer.lock with the specific version composer found that fit your version constraints.
- The new package has its source code installed in vendor/
- Any dependencies that package had are installed in vendor/
- Any dependencies that package had are added to the composer.lock file. The versioning constraints of dependencies are determined by the package’s composer.json NOT your project’s composer.json.
- If a dependency is found in both an external package’s composer.json and your composer.json, the versions must be compatible otherwise an error is thrown.
Removing Packages
composer remove VENDOR_NAME/MODULE_NAME
It’s easy to remove. When you issue the remove command:
- The entry in composer.json is removed
- The entry in composer.lock is removed
- Any dependencies required by only that module are also removed
composer remove VENDOR_NAME/MODULE_NAME --no-update
This command will remove only the given package, and not its dependencies.
Updating Packages
Alright, this is where 9/10 issues with composer arise. Composer makes the below command too easy to issue (in my opinion)
composer update
Looks harmless enough, eh? This command is equivalent to apt-get do-release-upgrade
- Go through every installed package and upgrade it to the latest stable version that can be satisfied by the given platform requirements and other installed modules
- Go through every dependency of every installed package and do the same
- This will break your website!!!!
Basically, you are telling composer to upgrade anything you have installed. So, this command should be used with care and only done if your intention is to update a package.
Just like require, we can do multiple packages at once:
composer update vendor_one/module_one vendor_two/module_two vendor_three/*
You can provide a * to upgrade all modules for a given vendor that are currently installed.
composer update --interactive
You can pass an interactive flag to go through each package interactively
composer update --dry-run
Finally, you can use the –dry-run flag to look at what an update would do, without actually doing it.
Catching Up Your Environment
Take the below scenario for a ticket. In this scenario, Jeff is tasked with upgrading the version of the Acme anti-fraud module installed on a site, and Sara is trying to incorporate his changes:
(1) Jeff checks out a version of the codebase with the Acme anti-fraud module installed at version 2.2 as a composer dependency
(2) Jeff will run composer install to get the Acme module installed in vendor/
(3) Jeff will run composer update acme/module-connect to upgrade the module to the latest stable version.
(4) If Sara wants to incorporate Jeff’s change, she will pull down the changes to composer.json and composer.lock via git pull origin BRANCH
(5) …Should Sara run (a) composer install or (b) composer update?
…… (a) composer install
Because Jeff upgraded Acme anti-fraud, the new version is reflected in composer.lock – so all that is needed by Sara is a composer install to catch-up her environment.
composer update = safe to run, never
Merge Conflicts with Composer
Alright, merge conflicts with composer are my least favorite part of working with it. Here is what typically happens with a merge conflict (in a hypothetical)
(1) Jeff runs composer require acme/module-fraud
(2) Sara runs composer require acme/module-tax
(3) Jeff merges in Sara’s branch
At this point (from the perspective of Jeff’s merge) he’ll see a conflict like this in composer.json
<<<<<<<<<<<<< HEAD
"acme/module-fraud": "*"
==================
"acme/module-tax": "*"
>>>>>>>>>>>>> refs/SARAS-BRANCH
Easy enough right? The issue is with composer.lock. Because each module had multiple dependencies and updated the lock file, you may see literally dozens of merge conflicts in the lock file.
And, to make matters worse, you may see conflicts between the timestamps in composer.lock and the content_hash – you really have no way of resolving the content_hash conflict without recomputing it.
Here is the method I recommend for resolving conflicts in the composer files, again from the perspective of Jeff merging in Sara’s branch
git checkout --ours composer.json composer.lock
composer require acme/module-tax
git add -A && git commit
So what are we doing here? In essence, we are undoing the composer require call from one of the branches, and then repeating it on top of the other branch.
Since we repeat the second composer require on top of the current branch, Git no longer sees it as a conflict (one require statement happened after the other chronologically).
And, we allow composer to naturally update the lock file without having to try and resolve those conflicts ourselves.
Now, if Sara’s branch represents a massive change to composer (many library upgrades) and your branch only changed one thing, you may prefer to checkout her version and keep your own:
git checkout --theirs composer.json composer.lock
composer require acme/module-fraud
git add -A && git commit
Notice the difference between –ours vs –theirs. These are shortcut pointers to the most recent SHA commit on either our branch, or the one we’re merging in.
Advanced Usage
There are a lot of features and options built into composer that we haven’t used. If you feel inspired, I recommend reading the docs: https://getcomposer.org/doc/
That said, I want to touch on a couple things:
Ignoring “platform requirements”
Every once in a while, you will want to install a module that claims your environment has an unsupported version of PHP. For example, perhaps a developer released a module that works on PHP 7.0, PHP 7.1, PHP 7.2 and PHP 7.3 — but, they only labeled it as supporting PHP 7.0
If you try to install that module, you will get an error saying composer cannot install it because your environment has the wrong version of PHP.
Well, what you should definitely not do is downgrade your version of PHP. Instead, you can use a flag to tell composer “I know I have the wrong version of PHP, but I just don’t care”
composer require vendor/module --ignore-platform-reqs
This command can also be used to ignore missing PHP module (for example, ext-bcmath, ext-curl, etc). Note that this really should only be used if the developer made a mistake in specifying a platform requirement. If you just force composer to install a module that really doesn’t work in your environment, you’ll introduce a bigger problem into the application.
Using composer to override a vendor file
There may be a situation where you need to modify a file stored in the vendor folder. For example, on Magento 2, you may need to override a core file but cannot use a <preference> or a <plugin>. When that happens, you have a couple options. The first one is to override the entire file.
This is similar to a local copy / local override in Magento 1:
cp vendor/some/file.php app/code/Cadence/SomeModule/some/file.php
You then update composer.json to look like the below example:
diff --git a/composer.json b/composer.json
index ae6c0233..543c94b8 100644
--- a/composer.json
+++ b/composer.json
@@ -34,7 +34,8 @@
},
"files": [
- "app/etc/NonComposerComponentRegistration.php"
+ "app/etc/NonComposerComponentRegistration.php",
+ "app/code/Cadence/Example/Framework/Archive/Tar.php"
],
In essence, you update the files property inside the autoload property. To understand why this works, we should look at how composer normally autoloads files. In PHP, when you reference a class name that doesn’t exist, the PHP engine uses the autoload logic you provide it to find the source code for that class (or throw an error if it can’t):
For example, when looking for the file that corresponds to the class
Magento\Framework\Archive\Helper\File\Tar
Composer is going to find the file by following the below autoload directives:
"autoload": {
"psr-4": {
"Magento\\Framework\\": "lib/internal/Magento/Framework/",
"Magento\\Setup\\": "setup/src/Magento/Setup/",
"Magento\\": "app/code/Magento/"
},
"psr-0": {
"": ["app/code/"]
},
"files": [
"app/etc/NonComposerComponentRegistration.php",
"app/code/Cadence/Example/Framework/Archive/Tar.php",
],
"exclude-from-classmap": ["**/dev/**", "**/update/**", "**/Test/**"]
},
If Magento is installed in app/code:
Normally, it would autoload the file by using the psr-0 or psr-4 mapping. Those mappings say:
- If the class starts with Magento\Framework look in lib/internal/Magento/Framework
- If the class starts with Magento look in app/code/Magento
- If the class has no namespace (“”) look in app/code
If Magento is installed in vendor/:
Now, there is an unseen autoloading property composer would actually use if Magento 2 was installed in the vendor/ folder:
- Go through each external package, and examine its composer.json to see how that package’s modules should be loaded.
- In this situation, the magento/framework module will supply autoloading logic to resolve that class.
Explicit File Autoloading:
But, we added a special file directive that says:
- Look for a class in the exact path: “app/code/Cadence/Example/Framework/Archive/Tar.php”
So, in this way, we ensure composer will find our version of the Tar.php file.
If you change the “files”: [] array, other people using the application must run the below special command to see your change:
composer dump-autoload
This forces composer to update the autoload logic in the vendor/ folder. This normally happens every time you do composer install – but in this case, you didn’t install anything, you just changed the autoload logic.
Using composer to patch a vendor file
I just want to touch briefly on this topic. Magento 2 utilizes composer patches for a couple core files. You can add your own composer patch files.
How do patch files work?
- You must have
cweagans/composer-patches
installed - You must supply the location of a patch file in the “extra” -> “patches” node in composer.json. This patch file must be mapped to a module installed in vendor/
- The patch file must note the relative file path to be updated.
Composer will now automatically patch a vendor/ file with the patch file you supplied after a composer install
For example:
"extra": {
"magento-force": "override",
"composer-exit-on-patch-failure": true,
patches": {
"magento/framework": {
"WP Translation Integration": "app/code/Cadence/External/patches/composer/__.diff"
}
}
}
This is code from Cadence Labs’ Magento-WordPress integration. It tells Magento we’re going to patch the magento/framework module after composer install.
If we look at that patch file, it looks like the below:
--- Phrase/__.php 2019-01-12 14:28:30.342725906 -0500
+++ Phrase/__.php 2019-01-12 15:03:43.214295471 -0500
@@ -18,5 +18,12 @@
$argc = $argc[0];
}
- return new \Magento\Framework\Phrase($text, $argc);
+ if (\Magento\Framework\Code\Generator\Autoloader::isMagentoTranslationMode() || !defined('WP_USE_THEMES')) {
+ return new \Magento\Framework\Phrase($text, $argc);
+ } else {
+ if (!is_string($argc)) {
+ $argc = 'default';
+ }
+ return translate($text, $argc);
+ }
}
As you can see, our patch file modifies the __() function to ensure it works consistently between Magento and WordPress.
This is an advanced topic. The best way to understand it is to try it out next time you need to modify something in vendor/
Conclusion
Composer is a powerful and complex tool for managing dependencies within your PHP project. We hope this tutorial will come in handy the next time you have a composer conflict!
Need help with Magento 2 or Composer?
We’ve had a lot of experience building websites with PHP. If you need help, head over to the Cadence Labs contact page, or email us at [email protected].