Lesson 2 of 9 · 20m
Composer Without the Hangover: Versions, Lock Files, and Autoloading
Version constraints that mean what you think, lock-file discipline, PSR-4, and auditing deps.
Every PHP codebase you touch in the wild either has a healthy composer.json or is a zip file somebody downloaded in 2014 and nobody wants to touch. Composer is the difference, and using it well is mostly about three disciplines: version constraints, the lock file, and the autoloader.
Constraints that mean what you think
composer.json declares intent; composer.lock records reality. The caret constraint ^2.4 means >=2.4.0 <3.0.0 — semver-compatible updates. The tilde ~2.4.1 means >=2.4.1 <2.5.0 — tighter. For applications (things you deploy), commit composer.lock and run plain composer install on deploys: you get the exact versions CI tested. For libraries (things others depend on), don't commit the lock and keep constraints wide.
{
"require": {
"php": ">=8.2",
"monolog/monolog": "^3.7",
"guzzlehttp/guzzle": "^7.8"
},
"require-dev": {
"phpunit/phpunit": "^11.0",
"phpstan/phpstan": "^1.12"
}
}Pin the php version explicitly — it makes composer install fail on a box running the wrong interpreter instead of exploding later on a missing syntax feature.
Autoloading: PSR-4 or don't bother
"autoload": {
"psr-4": { "App\\": "src/" }
}App\Http\Router maps to src/Http/Router.php. Match the case exactly — on Linux filesystems the lookup is case-sensitive, so Router.php vs router.php works on your Mac and 500s in production. That's the bug that ships.
For production builds:
composer install --no-dev --optimize-autoloader --classmap-authoritative--no-dev skips PHPUnit and friends (smaller attack surface, faster). --classmap-authoritative turns autoloading into a hash lookup — no filesystem scanning per class. The cost: new classes don't autoload until you dump again, so only use it in the deploy pipeline, not on your laptop.
Auditing dependencies
Run composer audit — it checks your lock file against the PHP Security Advisories database (FriendsOfPHP) and exits non-zero on known CVEs. Wire it into CI. When it fires, composer update <package> to the patched version, review the diff in the lock, and commit.
Try this
Take any project, change one constraint from * or a bare version to a proper ^x.y range, run composer update, and diff composer.lock. Then run composer audit and composer validate --strict. If validate complains about your name/description fields, fix them now — packagist-grade hygiene pays off later.
Field notes
- Never commit
vendor/. It belongs in the deploy artifact, not git. Except: no network at deploy time — then build an archive artifact, still not the repo. minimum-stability: devis a smell. It lets unstable versions of everything in; prefer pinning the one dev package you need with@dev.- Running
composer installas root inside a container triggers a warning for a reason — run it as the app user, or setCOMPOSER_ALLOW_SUPERUSER=1knowingly in a build-only stage that never ships the binary to production. composer outdated --directshows which of your declared deps are behind, without transitive noise.