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

  1. 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.
  2. 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 at src/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 run composer install to download the new versions specified in the composer.lock file.
  • Divide your project’s dependencies into requirements for production and requirements for development using the require and require-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 and autoload-dev keys. There is no reason to autoload the migrations and seeds 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: