Composer was just something I used to get my project up and running and occasionally install additional libraries. I never put too much thought into it – it just worked. Sometimes I would run into problems, but often they were easily fixed by running composer install
or composer dump-autoload
. I had no idea what dump-autoload was doing, but it was fixing things.
This article goes through everything I would have liked to know about composer. You'll find out why we are using it, how it works, what else you can do with it, and how to use it in production.
The pear days
Before composer, we relied on cherry-picking code from one place to another, or manually download libraries and throw them into a libs
directory. Another solution was PEAR (PHP Extension and Application Repository), the existing library distribution at the time.
But there were many problems with it:
- installations were made system-wide, rather than on a project basis. You couldn't have to versions of the same library on your machine.
- every library had to be unique. You were not allowed to have a different take on how to solve a specific problem.
- to have your repository accepted into PEAR you had to gather a certain number of up-votes.
- existing solutions were often outdated, inactive, or unmaintained.
Enter composer
Built by Nills Adermann and Jordi Boggiano, it solved everything PEAR sucked at. Packages were installed on a per-project basis and anyone was free to contribute, create, and share packages with the world. This encouragement led to more polished, complete, and bug-free libraries.
Composer is a dependency manager. It relies on a configuration file (composer.json
) to figure out your project's dependencies and their sub, sub-dependencies, where to download them from, and when to install and autoload them.
Dependencies are not only php libraries. They might also come in the form of platform requirements: like the php version and the extensions installed on your machine. These cannot be installed via composer – their only purpose is to inform developers on how their environment should look like.
Where are packages coming from?
Packages can be installed from anywhere as long as they are accessible through a VCS (version control system, like git or svn), PEAR, or a direct url to a .zip
file. To do so, you must specify your sources under the repositories
key using the proper repository configuration.
For private repositories, you can configure composer to use access tokens or basic http authentication. Configuration options are available for GitHub, Gitlab and Bitbucket.
If no repositories were specified, or packages were not found using the provided sources, composer will search its main repository, packagist.org.
Once the package is located, composer uses the VCS's features (branches and tags) to find and attempt to download the best match for the version constraints specified in the composer.json
file.
Wait a minute. Attempt? Best match?
- a confused reader
Well, yes. There are different ways to specify what package versions should composer install. But before we get into that, let's have a quick look at how most developers version their packages – which is by following the SemVer (semantic versioning) convention.
SemVer helps developers communicate the nature of the changes made to their package. This way, everyone relying on it won't have to manually check the source code for changes that might break their project.
The convention assumes a three-part version number X.Y.Z (Major.Minor.Patch) with optional stability suffixes. Each number starts at 0 and is incremented based on what type of changes were made:
- Major.Minor.Patch – increments when breaking changes were introduced
- Major.Minor.Patch – increments when backwards-compatible features were added
- Major.Minor.Patch – increments when bugs were fixed
- occasionally, stability suffixes are used:
-dev, -patch (-p), -alpha (-a), -beta (-b) or -RC (release candidate)
Cautions
- Packages having the major version at 0.x.x can introduce breaking changes during minor releases. Only packages having version 1.0.0 or higher are considered to be production-ready.
- Not all packages follow semantic versioning. Make sure to consult their documentation before making any assumptions.
Specifying version constraints
There are many ways you can specify package versions, but the most common ones are:
- version range – using the math operators >, >=, <, <=, !=
=1.0.0 <2.0.0
– will install the newest version higher or equal to 1.0.0 but lower than 2.0.0.
- caret range – adding a caret ^
will install the newest version available that does not include breaking changes.
^2.1.0
translates into "install the newest version higher or equal to 2.1.0, but lower than 3.0.0"
- tilde-range – is similar to the caret range; the difference is that it only increases the version of the last specified number.
~2.1
will install the newest 2.x version available (for example, 2.9
)
~2.1.0
will install the newest 2.1.x version available (for example, 2.1.9
)
- you can also choose to specify the exact version.
You can register dependencies in two different places: require
and require-dev
. The first contains everything your project needs to run in production, while the second dictates the additional requirements to do development work – for example, phpunit
to run your test suite. The reason why we specify dependencies in two different places is to not install and autoload packages intended for development on the production machines.
Here's what you might have in composer.json
:
{
"require": {
"php": "^7.1.3",
"ext-gd": "",
"lib-libxml": "",
"monolog/monolog": "^1.12"
},
"require-dev": {
"fzaninotto/faker": "^1.4",
"phpunit/phpunit": "^7.5"
}
}
Production requirements: php version between 7.1.3 and 8.0.0 (not included). Both the gd
(graphics drawing) extension and libxml
library must be installed, along with any version between 1.1.2 and 2.0.0 (not included) of the monolog/monolog package
.
Development requirements: fzaninotto/faker
between 1.4 and 2.0 (not included) and phpunit/phpunit
between 7.5 and 8.0 (not included).
Why version ranges?
Why would I ever want to set a range constraint and not the exact version?
- a confused reader
Specifying the exact versions makes it difficult to keep your project's dependencies up to date, introducing the risk of missing important patch releases. Using a range will allow composer to pull in new releases containing bugfixes and security patches.
Ok, but everybody makes mistakes. Some packages might still introduce breaking changes in minor releases. Doesn't that mean that when I'll run composer install on the production machine, there is a chance my project will break due to the breaking changes introduced by some random package?
- a concerned reader
Here's where the composer.lock
file kicks in. Once you ran the install for the first time, the exact versions of your dependencies and their sub-dependencies are stored in the composer.lock
file – meaning that all subsequent installs will download the exact same versions, avoiding the scenario above.
The only time composer tries to guess what package versions to download is when you run the install command for the first time or when the composer.lock
file goes missing. Unless you are writing a library, you should always commit the composer.lock
file to source control.
Running the composer update command will act as if the lock file doesn't exist. Composer will look up any new versions that fit your versioning constraints and rewrite the composer.lock
file with the new updated versions.
Composer autoloading
Composer generates a vendor/autoload.php
file you can include in your project and start using the classes provided by the installed packages without any extra work. You can even add your own code to the autoloader by adding an autoload
key to composer.json
.
Here’s an example of what you can autoload using composer:
{
"autoload": {
"psr-4": {
"Foo\\": "src/"
},
"classmap": [
"database/seeds",
"database/factories",
"lib/MyAwesomeLib.php"
],
"files": ["src/helpers.php"],
"exclude-from-classmap": ["tests/"]
}
}
- PSR-0 and PSR-4 are standards used to translate class namespaces into the physical paths of the files containing them. For example, whenever we import the
use Foo\Bar\Baz;
class, composer will autoload the file located atsrc/Foo/Bar/Baz.php
. - classmap – contains a list of class files and directories to autoload.
- files – contains a list of files to autoload. This is especially useful when you have a bunch of functions you want to use globally throughout your project.
- you can also exclude certain files and directories from being autoloaded by adding them under
exclude-from-classmap
.
What about custom scripts?
A composer script can be a PHP callback defined as a static method call or any other valid command-line executable command. They can either be run manually, using composer yourScriptName
or hooked into different events thrown during the composer execution process.
For example, after creating a new Laravel project, composer makes a copy of the .env.example
file to .env
and then runs the php artisan key:generate
command to generate and set the application key in the .env
file it just copied.
{
"scripts": {
"post-root-package-install": [
"@php -r \\"file_exists('.env') || copy('.env.example', '.env');\\""
],
"post-create-project-cmd": [
"@php artisan key:generate --ansi"
]
}
}
You can also reference other composer scripts. In the test
script bellow, we call the clearCache
script to delete the cache directory before running our tests using phpunit
.
{
"scripts": {
"test": [
"@clearCache",
"phpunit"
],
"clearCache": "rm -rf cache/*"
}
}
Composer in production
Here are a set of guidelines I recommend you to follow when using composer in production environments:
- Never, ever run
composer update
in production. Do it on on a development machine so you can make sure everything is still working. Only then commit your changes, pull them on the production machine and runcomposer install
to download the new versions specified in thecomposer.lock
file. - Divide your project’s dependencies into requirements for production and requirements for development using the
require
andrequire-dev
keys. This way composer will not install packages intended for development (eg: phpunit) in a production environment. - Make sure you only autoload the files and directories you need. As with the requirements, you can also split the autoloading into production and development using the
autoload
andautoload-dev
keys. There is no reason to autoload themigrations
andseeds
directories in production. - Use the
composer install --no-dev --optimize-autoloader
to install packages and optimize the autoloader for production. The--no-dev
flag instructs composer to ignore development-only packages, while the--optimize-autoloader
flag converts the dynamic PSR-0/PSR-4 autoloading into a static classmap. This makes loading files faster because with classmap the autoloader knows exactly where a file is located, while when using PSR-0/PSR-4, it always has to check whether that file exists or not.
There you have it. Everything there is to know about composer – at least from the user’s point of view. If you’re interested in how to create and publish a package on packagist.org, this is a good tutorial to follow.
You should also read these:
- Blast from the Past – the transition from PEAR to composer, PEAR being hacked and whether it’s still relevant or not (spoiler alert, it’s not).
- The rise of Composer and the fall of PEAR – by Fabien Potencier, the Symfony framework creator, on how they moved from pear to composer.
- 24 tips for using Composer efficiently – the title says it all.