Skip to content

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: dev is a smell. It lets unstable versions of everything in; prefer pinning the one dev package you need with @dev.
  • Running composer install as root inside a container triggers a warning for a reason — run it as the app user, or set COMPOSER_ALLOW_SUPERUSER=1 knowingly in a build-only stage that never ships the binary to production.
  • composer outdated --direct shows which of your declared deps are behind, without transitive noise.