Composer and Autoloading
Before Composer, PHP had no way to say "this project needs that library at that version". Third-party code was copy-pasted into the repository by hand, and PEAR installed packages globally, machine-wide. Composer changed this twice over. First, it made the dependency set declarative and per-project: you declare what you need, it works out which concrete versions are mutually compatible, and it lays them out under vendor/. Second — and this mattered more — it brought a single autoloading convention. That is why libraries from a dozen different vendors are wired in today by one line, require 'vendor/autoload.php', instead of a hundred requires, one per file.
Both of the topic's central traps grow out of that same pair — version resolution plus autoloading — so let us name them upfront. First: composer.json declares ranges, while composer.lock records the exact versions that actually resolved, transitive ones included. You commit the lock and run install, never update, in CI and on deploy — otherwise reproducibility is gone and production is built from something you never tested. Second: autoloading is not free. Plain PSR-4 turns a class name into a path and goes to the filesystem — for every class seen for the first time, on every request. In production you turn that into a static class map, and a lookup collapses into a single array read. The layer-by-layer breakdown is below.
Topic map
- Composer and its files — what problem a dependency manager solves, what
composer installdoes, what lives invendor/, and howrequirediffers fromrequire-dev. - composer.lock and semver — what the lock file pins, how
installdiffers fromupdate, and how the^and~constraints actually read. - Namespaces — how a namespace makes a class name unique, what
usereally does, and whynew Exceptioninside a namespace breaks. - PSR-4 autoloading — how a class name becomes a path on disk, what that costs, and what the optimized classmap changes.
- The PSR standards — what PHP-FIG is, what a PSR standardizes, and how shared interfaces decouple a library from a framework.
Common Mistakes and Traps
| Mistake | Consequence |
|---|---|
Not committing composer.lock | Every install re-resolves versions — production is built from a set that never passed the tests |
Running composer update in CI or on deploy | It pulls fresh versions past your tests; building the pinned set is what install is for |
Believing ^1.2.3 allows 2.0.0 | ^ stays inside the major — up to but excluding 2.0.0; a major is bumped by hand |
Reading ~1.2.3 as "any update" | A three-digit tilde moves the patch only — up to 1.3.0 |
Committing vendor/ to git | It is build output; you reproduce it from the lock file, you do not store it |
| Installing dev dependencies in production | PHPUnit and fixture builders are not needed there and only widen the attack surface — that is what --no-dev is for |
| Leaving an unoptimized autoloader in production | Filesystem hits for every new class; --optimize-autoloader collapses them into one array read |
Turning on --classmap-authoritative where classes are generated at runtime | Classes outside the map do not exist as far as the autoloader is concerned — Class not found instead of a load |
Treating a top-of-file use as including a file | use is a compile-time alias; it loads nothing at all |
Writing new Exception inside a namespace | It looks for App\Exception; class names do not fall back to the global namespace — you need \Exception or a use |
| Thinking a PSR is part of the language | A PSR is a PHP-FIG agreement; the engine never checks it |
| Changing a public method signature in a minor release of a package | Consumers pinned with ^ pick it up on their next composer update and break |
Interview relevance
Composer is the topic that quickly reveals whether a candidate has worked on a team and seen production. The composer.lock question is asked almost always, and "it is a file Composer creates" is not an answer. The answer goes like this: "composer.json is what we ask for, composer.lock is what resolved, down to the transitive dependencies; that is why the lock is committed and why production and CI run install." A candidate who adds that --no-dev shrinks vendor/ and --optimize-autoloader removes the filesystem hits immediately reads as someone who has shipped releases themselves.
Typical checks:
- What
composer installdoes and how it differs fromcomposer update. - What
composer.lockpins and why it is committed. - The difference between
^and~, and what happens to^on a0.xversion. - How
requirediffers fromrequire-devand what--no-devbuys on a deploy. - How PSR-4 maps a fully-qualified class name onto a path, and what that costs.
- What PHP-FIG is and why PSR-3 or PSR-7 decouple a library from a framework.
- How to ship a breaking change in an internal package.
Common wrong answer: "composer install and composer update are roughly the same, update is just fresher." That is usually where the conversation opens up: install reproduces a pinned set of versions, update re-resolves dependencies and rewrites the lock file. Running update on a deploy means shipping a combination of packages nobody has ever tested. The second classic failure is being convinced that ~1.2.3 and ^1.2.3 mean the same thing.