Although most PHP developers know how to use Composer, not all of them are using it efficiently or in a best possible way. So I decided to summarize things which are important for my everyday workflow.
The philosophy of most of the tips is "Play it safe", which means that if there are more ways how to handle something, I would use the approach which is least error-prone.
Tip #1: Read the documentation
I really mean it. The documentation is great and a few hours of reading it will save you more time in the long run. You would be surprised how many things Composer can do.
Tip #2: Be aware of differences between a "project" and a "library"
It's important to know, whether you are creating a "project" or a "library". Each of them requires separate set of practices.
A library is a reusable package, that you would add as a dependency - such as symfony/symfony
, doctrine/orm
or elasticsearch/elasticsearch
.
A project is typically an application, that depends on several libraries. It is usually not reusable (no other projects would require it as a dependency). Typical example is an ecommerce website, customer support system etc.
I will distinguish between library and a project in the tips bellow.
Tip #3: Use specific dependencies' versions for applications
If you are creating an application, you should use the most specific version to define the dependency. If you need to parse YAML files, you should specify the dependency like this "symfony/yaml": "4.0.2"
.
Even if the library follows Semantic Versioning, there may be backwards-compatibility breaks in the minor and patch versions. For example, if you are using "symfony/symfony": "^3.1"
, there may be something deprecated in 3.2 which may break your application tests. Or there may be a bug fixed in PHP_CodeSniffer and it would detect new formatting issues in your code, which again may lead to a broken build.
The update of dependencies should be deliberate, not accidental. One of the tips bellow discusses it in greater detail.
It may sound as an overkill, but it will prevent your co-workers from accidentally updating all dependencies when adding a new library to project (which you may miss during Code Review).
Tip #4: Use version ranges for libraries dependencies
If you are creating a library, you should define the broadest version range possible. If you create a library that uses symfony/yaml
library for YAML parsing, you should require it like this:
"symfony/yaml": "^3.0 || ^4.0"
It means that your library can utilize symfony/yaml
from any Symfony 3.x or 4.x versions. It is important, because this constraint is passed to the application that uses your library.
In case there are two libraries with conflicting requirements, e.g. one requires ~3.1.0
and other requires ~3.2.0
, the installation would fail.
Tip #5: You should commit composer.lock
to git in applications
If you are creating a project, you definitely want to commit composer.lock
to git. This ensures that everyone - you, your co-workers, your CI server and your production server - is running the application with the same dependencies versions.
At first glance, it may sound superfluous - you are already using a specific version in the constraint, as mentioned in the tip #3. But no, there are also the dependencies of your dependencies which are not bound by these constraints (e.g. symfony/console
depends on symfony/polyfill-mbstring
). So without committing the composer.lock
, you won't get the exact same set of dependencies.
Tip #6: Put composer.lock
into .gitignore
in libraries
If you are creating a library (let's call it acme/my-library
), you should not commit a composer.lock
file. It does not have any effect on the projects that are using your library.
Imagine that the acme/my-library
uses monolog/monolog
as a dependency. If you have committed a composer.lock
, everyone who is developing the acme/my-library
would be using an older version of Monolog. But when the library is finished, and you use it in a real project, a newer version of Monolog may be installed, and it may not be compatible with the library. But you didn't notice it before, because of the composer.lock
!
It is best to put composer.lock
into your .gitignore
so you won't commit it accidentally.
If you want to make sure that the library is compatible with different versions of its dependencies, read the next tip!
Tip #7: Run Travis CI builds with different versions of dependencies
This tip applies only to libraries (because you use specific versions for applications).
If you are building an open-source library, you are probably using Travis CI for running its builds.
By default, Composer installs the latest possible versions of dependencies which are allowed by the constraints in composer.json
. It means that for the dependency constraint ^3.0 || ^4.0
, the build would always use the latest version of the v4 release. As the 3.0 is never tested, the library may not be compatible with it and that would make your users sad.
Luckily, Composer provides a switch to install the lowest possible versions of dependencies --prefer-lowest
(should be used with --prefer-stable
to prevent installation of non-stable versions).
Updated .travis.yml
configuration may look like this:
language: php
php:
- 7.1
- 7.2
env:
matrix:
- PREFER_LOWEST="--prefer-lowest --prefer-stable"
- PREFER_LOWEST=""
before_script:
- composer update $PREFER_LOWEST
script:
- composer ci
See it live in my mhujer/fio-api-php library and the build matrix on Travis CI
Even though this solution would catch most of the incompatibilities, remember that there are many combinations of dependencies between lowest and latest versions. And they may be incompatible together.
Tip #8: Sort packages in require and require-dev by name
It is a good practice to keep packages in require
and require-dev
sorted by name. It can prevent unnecessary merge conflicts when rebasing a branch. Because if you have added a package to the end of the list in two branches, there would be a merge conflict every time.
It is a tedious task to do manually, so it is best to configure it in composer.json
:
{
...
"config": {
"sort-packages": true
},
…
}
Next time, you require
a new package, it will be added to a proper place (and not to the end).
Tip #9: Do not attempt to merge composer.lock
when rebasing or merging
If you add a new dependency to composer.json
(and composer.lock
) and before your branch is merged, there is another dependency added in master, you need to rebase your branch. And you will get a merge-conflict in composer.lock
.
You should never try to resolve this conflict manually, because the composer.lock
file contains a hash of dependencies defined in composer.json
. So even if you resolve the conflict, the resulting lock file would be incorrect.
Best thing to do is to create .gitattributes
in the project root with the following line, which means that the git won't even attempt to merge the composer.lock
:
/composer.lock -merge
You can remedy this issue by using short-lived feature branches as suggested in Trunk Based Development (you should be doing this anyway). When you have a short-lived branch, which is merged promptly, the risk of merge conflict in composer.lock
is minimal. You may even create a branch just for adding a dependency and merge it right away.
But what to do, if you encounter a merge conflict in composer.lock
when rebasing? Resolve it with the version from master, so you will have changes only in composer.json
(the newly added package). And then run composer update --lock
, which will to update the composer.lock
file with changes from composer.json
. Now you can stage the updated composer.lock
and continue with the rebase.
Tip #10: Know the difference between require
and require-dev
It is important to be aware of the difference between require
and require-dev
blocks.
Packages which are required to run the application or library should be defined in require
(e.g. Symfony, Doctrine, Twig, Guzzle, …). If you are creating a library, be careful about what you put to require
. Because each dependency from this section is also a dependency of the application, which uses the library.
Packages necessary for developing the application (or library) should be defined in require-dev
(e.g. PHPUnit, PHP_CodeSniffer, PHPStan).
Tip #11: Update dependencies safely
I guess we can agree on the fact that dependencies should be updated regularly. What I want to discuss here is that dependencies updating should be explicit and deliberate, not done just by-the-way with some other work. If you refactor something and at the same time update some library, you can't easily tell if the app was broken by the refactoring or by the update.
You can use composer outdated
command to see what dependencies can be updated. It is a good idea to include --direct
(or -D
) switch to list only dependencies specified in composer.json
. There is also a -m
switch to list only minor version updates.
For each outdated dependency follow these steps:
- Create a new branch
- Update the dependency version in
composer.json
to the latest one - Run
composer update phpunit/phpunit --with-dependencies
(replacephpunit/phpunit
with the library you are updating) - Check the CHANGELOG in the library repository on Github to see if there are any breaking changes. If so, update the application
- Test the application locally (If you are using Symfony, you can find deprecation warnings in the Debug Bar)
- Commit the changes (
composer.json
,composer.lock
and anything else what was necessary for new version to work) - Wait for the CI build to finish
- Merge and deploy
Sometimes it makes sense to update more dependencies at once, e.g. when you are updating Doctrine or Symfony. In this case you can list them all in update command:
composer update symfony/symfony symfony/monolog-bundle --with-dependencies
Or you can use a wildcard to update all dependencies from a specific namespace:
composer update symfony/* --with-dependencies
I know that this all sounds tedious, but you will probably update dependencies just occasionally, so it is worth the extra safety.
One shortcut which is acceptable to make is to update all require-dev
dependencies at once (if they do not require changes in the code, otherwise I would suggest using separate branches for easier code review).
Tip #12: You can define other types of dependencies in composer.json
Apart from defining libraries as dependencies, you can also define other things there.
You can define, which PHP versions your application/library supports:
"require": {
"php": "7.1.* || 7.2.*",
},
You can also define which extensions are required for the application/library. It is super-useful when you are trying to dockerize your application or your new colleague is setting-up the application for the first time.
"require": {
"ext-mbstring": "*",
"ext-pdo_mysql": "*",
},
(You should use *
for the extensions version as they may be a bit inconsistent).
Tip #13: Validate the composer.json
during the CI build
composer.json
and composer.lock
should be always kept in sync. Therefore, it is a good idea to have an automatic check for it. Just add this as a part of you build script and it will ensure that composer.lock
is in sync with composer.json
:
composer validate --no-check-all --strict
Tip #14: Use a Composer plugin in PHPStorm
There is a composer.json plugin for PHPStorm. It adds autocompletion and some validations when changing composer.json
manually.
If you are using other IDE (or just a code editor), you can setup validation against its JSON schema.
Tip #15: Specify the production PHP version in composer.json
If you are like me and you are sometimes running pre-released PHP versions locally, you are in risk of updating the dependencies to a version that won't work in production. Right now, I'm using PHP 7.2.0 which means, that I can install libraries, that would not work on 7.1. As the production is running 7.1, the installation would fail there.
But no need to worry, there is an easy way out. Just specify the production PHP version in config
section of composer.json
:
"config": {
"platform": {
"php": "7.1"
}
}
Don't confuse it with require
section, which behaves differently. Your application may be able to run on 7.1 or 7.2 and at the same time specify 7.1 as a platform (which means that the dependencies will be always updated to a version compatible with 7.1):
"require": {
"php": "7.1.* || 7.2.*"
},
"config": {
"platform": {
"php": "7.1"
}
},
Tip #16: Using private packages from self-hosted Gitlab
It is recommended to use vcs
as a repository type and the Composer should determine the proper way of fetching the packages. For example, if you are adding a fork from Github, it would use its API to download the .zip file instead of cloning the whole repository.
But it is more complicated for a private Gitlab installation. If you use vcs
as a repository type, Composer will detect that it is a Gitlab installation would try to download the package using the API (which requires an API key. I didn't want to set it up, so I settled for this setup (which uses SSH for cloning):
First specify the repository with the type git
:
"repositories": [
{
"type": "git",
"url": "git@gitlab.mycompany.cz:package-namespace/package-name.git"
}
]
Then use the package as you would have normally:
"require": {
"package-namespace/package-name": "1.0.0"
}
Tip #17: How to temporarily use a branch with bugfix from fork
If you find a bug in some public library and you fix it in your fork on Github, you need to install the library from this repository instead of the official one (until the bugfix is merged and fixed version is released).
It can be done easily with inline aliasing:
{
"repositories": [
{
"type": "vcs",
"url": "https://github.com/you/monolog"
}
],
"require": {
"symfony/monolog-bundle": "2.0",
"monolog/monolog": "dev-bugfix as 1.0.x-dev"
}
}
You can test the bugfix locally before pushing it by using path
as a repository type.
Update 2018-01-08:
After publishing the article, I got suggestions for several more tips. So here they are:
Tip #18: Install prestissimo to speed up package installation
There is a Composer plugin hirak/prestissimo which speeds up dependencies installation by downloading them in parallel.
And the best thing? You only need to install it once, globally and it will work automatically for all projects:
composer global require hirak/prestissimo
Tip #19: Test your version constraints if you are not sure
Writing correct version constraints may sometimes be tricky even after reading the documentation.
Luckily, there is a Packagist Semver Checker where you can check which versions match the specified constraint. Instead of only analysing the version constraint, it downloads the data from Packagist to display the actual released versions.
See the result for symfony/symfony:^3.1
.
Tip #20: Use authoritative class map in production
You should generate authoritative class map in production. It will speed-up class loading by including everything in class-map and skipping any filesystem checks.
You can do it by running this as a part of your production build:
composer dump-autoload --classmap-authoritative
Tip #21: Configure autoload-dev
for tests
You don't want to include test files in production class map (because of the file size and memory). It can be done by configuring the autoload-dev
(similarly to autoload
):
"autoload": {
"psr-4": {
"Acme\\": "src/"
}
},
"autoload-dev": {
"psr-4": {
"Acme\\": "tests/"
}
},
Tip #22: Try Composer scripts
Composer scripts are a lightweight tool to create build scripts. I have written a separate article about them.
Tip #23: Use Composer CLI to remove packages
added on April 23th 2019
The best way to remove the package from composer.json
is by using a Composer command line interface. In the following example I'm removing phpoffice/phpspreadsheet
because it is not needed in the project anymore. You can see that its sub-dependencies which are no longer necessary are automatically uninstalled as well:
> composer remove phpoffice/phpspreadsheet
Package operations: 0 installs, 0 updates, 3 removals
- Removing phpoffice/phpspreadsheet (1.6.0)
- Removing markbaker/matrix (1.1.4)
- Removing markbaker/complex (1.4.7)
This is much better approach than removing it manually from composer.json
and running composer update
. That would make unrelated changes to the installed dependencies (see Tip #11 for more information on how to update the dependencies safely).
Tip #24: Do not use code from transitive dependencies
added on April 23th 2019
Imagine that you have required some library (e.g. AcmePayment) which in turn requires nette/utils
package (nette/utils
is called a transitive dependency in this case - it is a dependency of a dependency).
Because nette/utils
package provides lot of helper functions, you start to use them. But then you stopped using the AcmePayment library and removed it from the project. In an unexpected turn of events, nette/utils
was also removed, because it wasn't required in the project (from the Composer's point of view).
The solution is to include all libraries you use in the require
section of the composer.json
. There is a tool called ComposerRequireChecker which should be able to detect the missing ones.
Conclusion
If you disagree with some of the tips, I would be happy if you can describe why in the comments (don't forget to put the tip number there).