PHPComposer & Dependency Management

Composer & Dependency Management

Almost no PHP project is written entirely from scratch anymore. Logging, HTTP clients, database query builders, testing frameworks — all of it usually comes from a third-party library someone else already wrote and maintains. Composer is the tool the PHP ecosystem settled on for managing those libraries: it downloads the packages a project depends on, keeps track of exactly which versions are installed, resolves the tangled web of "this package needs that package" for you, and wires up autoloading so every installed class is available with a single require. If you've used npm for Node.js or pip for Python, Composer fills the same role for PHP.

Installing Composer

Composer is a standalone command-line program, not a PHP extension. On most systems it's easiest to install it globally so the composer command is available everywhere, though a project can also keep a local composer.phar file if a system-wide install isn't possible.

Checking Composer is installed

Bash
composer --version
# Composer version 2.7.x 2024-xx-xx

The exact installation steps differ by operating system — Windows has a dedicated installer executable, while Linux and macOS typically download a small install script from getcomposer.org and move the resulting composer.phar onto the system path. Once it's on the path, every command below works the same regardless of platform.

What composer.json describes

Every Composer-managed project has a composer.json file at its root. It's a plain JSON manifest that declares the project's dependencies, its own package metadata, and configuration like autoloading rules. The two dependency sections that matter most day to day are require, for packages the application needs to run, and require-dev, for packages only needed while developing — test frameworks, static analyzers, debugging tools — that have no business being installed on a production server.

composer.json — a small but realistic example

JSON
{
    "name": "acme/billing-service",
    "description": "Internal billing microservice",
    "require": {
        "php": "^8.2",
        "guzzlehttp/guzzle": "^7.8",
        "monolog/monolog": "^3.5"
    },
    "require-dev": {
        "phpunit/phpunit": "^11.0"
    },
    "autoload": {
        "psr-4": {
            "Acme\\Billing\\": "src/"
        }
    }
}

The version constraints (^7.8, ^3.5, and so on) aren't exact version numbers — they're ranges. A caret constraint like ^7.8 means "any 7.x release that is at least 7.8," which lets Composer pick up bug fixes and minor feature releases automatically without risking a breaking major-version upgrade.

Adding a dependency with composer require

Rather than hand-editing composer.json, the normal way to add a dependency is the composer require command. It looks the package up, figures out a version constraint compatible with everything already installed, downloads it into the vendor/ directory, and updates composer.json (and composer.lock, covered next) for you.

Adding a runtime and a dev dependency

Bash
composer require guzzlehttp/guzzle
composer require --dev phpunit/phpunit
Using version ^7.8 for guzzlehttp/guzzle
./composer.json has been updated
Running composer update guzzlehttp/guzzle
Generating autoload files
composer install vs. composer update

These two commands are the source of more confusion than anything else in Composer, because they sound interchangeable but behave very differently. composer install reads composer.lock — if one exists — and installs the exact versions recorded there, ignoring the version ranges in composer.json entirely. composer update does the opposite: it ignores the lock file, re-resolves every dependency against the ranges in composer.json, potentially pulls in newer releases, and then rewrites composer.lock to match.

Command

Reads

Writes

Typical use

composer install

composer.lock

vendor/ only

Setting up a project you just cloned, or deploying

composer update

composer.json

composer.lock + vendor/

Deliberately picking up new dependency versions

Why composer.lock exists

A version constraint like ^7.8 in composer.json is a promise about what's acceptable, not a record of what's actually installed. Without something pinning down the exact resolved versions, one teammate running composer install today and another running it next week could end up with two different patch releases of the same library, purely because a new version was published in between. composer.lock solves this by recording the precise version, and even the exact commit hash, of every package that was resolved the last time composer update ran. As long as composer.lock is committed to version control and everyone runs composer install (not update) to set up the project, every machine — a teammate's laptop, the CI server, production — ends up with byte-for-byte identical dependencies.

Always commit composer.lock for applications
For an application (as opposed to a reusable library published on Packagist), `composer.lock` should be committed to git alongside `composer.json`. Leaving it out of version control defeats its entire purpose: reproducible installs. Libraries are the one exception — they're usually published without a committed lock file, since the whole point of a library is to be installed with whatever compatible versions the *consuming* application has already resolved.
Packagist, the default package registry

When Composer looks up a package name like guzzlehttp/guzzle, it queries Packagist, the default public package repository for the PHP ecosystem, by default. Packagist is where the vast majority of open-source PHP packages are published, indexed by a vendor-name/package-name identifier that mirrors how the package author has organized it. A private company can also run its own Composer-compatible repository (Composer supports this via the repositories key in composer.json) for internal packages that shouldn't be public, but for anything open source, Packagist is almost always where it lives.

Removing a dependency

Bash
composer remove guzzlehttp/guzzle
  • Composer is a dependency manager, not a PHP extension — it is a separate command-line tool.

  • composer.json declares dependencies under require (runtime) and require-dev (development-only tools).

  • composer require vendor/package adds a dependency, resolves it, downloads it into vendor/, and updates the manifest.

  • composer install installs the exact versions recorded in composer.lock; composer update re-resolves against composer.json and rewrites the lock file.

  • composer.lock should be committed for applications so every environment gets identical dependency versions.

  • Packagist is the default public registry Composer queries when resolving a package name.

Tip
If a deploy or a teammate's environment ever ends up with different package versions than expected, the fix is almost never `composer update` — that makes things worse by pulling in whatever is newest right now. Run `composer install` instead, which trusts `composer.lock` and reproduces exactly what was already tested.