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
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
{
"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
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.
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
composer remove guzzlehttp/guzzle
Composer is a dependency manager, not a PHP extension — it is a separate command-line tool.
composer.jsondeclares dependencies underrequire(runtime) andrequire-dev(development-only tools).composer require vendor/packageadds a dependency, resolves it, downloads it intovendor/, and updates the manifest.composer installinstalls the exact versions recorded incomposer.lock;composer updatere-resolves againstcomposer.jsonand rewrites the lock file.composer.lockshould be committed for applications so every environment gets identical dependency versions.Packagist is the default public registry Composer queries when resolving a package name.