December 10, 2014

Why to version your composer.lock file

Composer.json often lists dependencies with version numbers that can point to a bunch of potential packages. Lines like this:

"require": {
    "laravel/framework": "4.2.*",
    "ruflin/elastica": "1.3.0.0",
    "guzzlehttp/guzzle": "~4.0",
    "sunra/php-simple-html-dom-parser": "1.5.0"
},

When composer install is run without a composer.lock file available, it has to translate the ~, *, =>, and other bits of flexible version numbering into an real version number to download for you. It also has to grab the dependencies of each one of the dependencies you specify. This takes time, memory and bandwidth. When composer finishes, it saves a list of the packages it has downloaded to composer.lock with real version numbers and all the required packages included.

Example Scenario

You start a new project by yourself. You set up composer.json with some packages, run composer install and commit all the right stuff to get, minus composer.lock

72 hours later…

Another developer on your team receives your code, including the composer.json. They run composer install and receive a different set of packages than you did 72 hours earlier because one of the dependencies pushed an update. Will that be a problem? Maybe…

72 hours later…

The project is done! Time to deploy. Somewhere in the process composer install runs. All the dependencies will be resolved again wasting bandwidth, memory, time, and potentially installing a different set up of packages then on either development machines. This won’t introduce bugs if you are lucky, but it isn’t optimal.

What’s inside composer.lock?

You’ll notice composer.lock is a much bigger file than composer.json (72kb vs 1kb for my example). Here’s what it looks like:

{
    "_readme": [
        "This file locks the dependencies of your project to a known state",
        "Read more about it at http://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file",
        "This file is @generated automatically"
    ],
    "hash": "675d6a1de44ac07f57f730785f25142b",
    "packages": [
        {
            "name": "classpreloader/classpreloader",
            "version": "1.0.2",
            "source": {
                "type": "git",
                "url": "https://github.com/mtdowling/ClassPreloader.git",
                "reference": "2c9f3bcbab329570c57339895bd11b5dd3b00877"
            },
            "dist": {
                "type": "zip",
                "url": "https://api.github.com/repos/mtdowling/ClassPreloader/zipball/2c9f3bcbab329570c57339895bd11b5dd3b00877",
                "reference": "2c9f3bcbab329570c57339895bd11b5dd3b00877",
                "shasum": ""
            },
            "require": {
                "nikic/php-parser": "~0.9",
                "php": ">=5.3.3",
                "symfony/console": "~2.1",
                "symfony/filesystem": "~2.1",
                "symfony/finder": "~2.1"
            },
            "bin": [
                "classpreloader.php"
            ],
            "type": "library",
            "extra": {
                "branch-alias": {
                    "dev-master": "1.0-dev"
                }
            },
            "autoload": {
                "psr-0": {
                    "ClassPreloader": "src/"
                }
            },
            "notification-url": "https://packagist.org/downloads/",
            "license": [
                "MIT"
            ],
            "description": "Helps class loading performance by generating a single PHP file containing all of the autoloaded files for a specific use case",
            "keywords": [
                "autoload",
                "class",
                "preload"
            ],
            "time": "2014-03-12 00:05:31"
        },
        {
            "name": "d11wtq/boris",
            "version": "v1.0.8",
            "source": {
                "type": "git",
                "url": "https://github.com/d11wtq/boris.git",
                "reference": "125dd4e5752639af7678a22ea597115646d89c6e"
            },
            "dist": {
                "type": "zip",
                "url": "https://api.github.com/repos/d11wtq/boris/zipball/125dd4e5752639af7678a22ea597115646d89c6e",
                "reference": "125dd4e5752639af7678a22ea597115646d89c6e",
                "shasum": ""
            },
            "require": {
                "php": ">=5.3.0"
            },
            "suggest": {
                "ext-pcntl": "*",
                "ext-posix": "*",
                "ext-readline": "*"
            },
            "bin": [
                "bin/boris"
            ],
            "type": "library",
            "autoload": {
                "psr-0": {
                    "Boris": "lib"
                }
            },
            "notification-url": "https://packagist.org/downloads/",
            "time": "2014-01-17 12:21:18"
        },

......[more packages listed here]....

    ],
    "aliases": [

    ],
    "minimum-stability": "stable",
    "stability-flags": {
        "mockery/mockery": 20
    },
    "prefer-stable": false,
    "platform": [

    ],
    "platform-dev": [

    ]
}

You’ll find all the packages in composer.json and the resolved dependencies listed, each with their version number. Why is this better? When composer install uses information from composer.lock, it simply downloads the listed packages, and doesn’t have to traverse through the dependency tree searching for packages, or figure out which version of a package to download. This saves time when a developer or server runs composer install, and ensures the same versions of every package are requested every time. Keep composer.lock in version control, and the team and production server will benefit from this when composer install is run.

More info

Read this conversation on GitHub:

KingCrunch commented on Feb 11

@paparts Sounds like you don’t versionize the composer.lock? As a rule of thumb: For applications versionize it, for libraries, don’t. You shouldn’t run update on a live system, because it is quite likely, that sooner or later a package comes in, that breaks your application, without that you’ve tested it locally. The composer.lock and composer.phar install ensures, that exactly that packages in that versions are installed, that you’ve development your application against.

paparts commented on Feb 12

I didn’t notice that the framework I was using has listed the composer.lock on the ignore list. Thanks for pointing that out.

Fatal error: Uncaught exception 'ErrorException' with message 'proc_open(): fork failed - Cannot allocate memory' in phar:///home/ubuntu/somefolder/composer.phar/vendor/symfony/console/Symfony/Component/Console/Application.php:985

Here’s an explanation of composer install and composer update and how they are different: http://adamcod.es/2013/03/07/composer-install-vs-composer-update.html.

Here’s the official composer documentation explaining what the lock file does: https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file