Composer & Autoloading
Dependency management with Composer — the lock file and semver constraints, PSR-4 autoloading and namespaces, require versus require-dev, the PHP-FIG interop standards, and optimizing the autoloader for production.
13 questions
JuniorTheoryVery commonWhat problem does Composer solve, and what does the vendor/ directory hold?
What problem does Composer solve, and what does the vendor/ directory hold?
Composer is PHP's per-project dependency manager. Required packages are declared in composer.json, and composer install resolves their versions, downloads them into vendor/, and generates vendor/autoload.php. The vendor/ tree is build output — you neither edit it nor commit it.
Common mistakes
- ✗Treating
vendor/as editable source rather than generated build output - ✗Committing
vendor/instead ofcomposer.jsonpluscomposer.lock - ✗Thinking Composer installs packages globally like a system package manager
Follow-up questions
- →What does
vendor/autoload.phpregister, and where is it included from? - →Why does
composer installbehave differently fromcomposer update?
JuniorTheoryVery commonWhy is composer.lock committed to version control, and what does it pin?
Why is composer.lock committed to version control, and what does it pin?
composer.json states version ranges; composer.lock records the exact version Composer actually resolved for every package, transitive ones included. Committing it makes composer install reproducible — CI, teammates and production all get identical dependencies. Only composer update re-resolves and rewrites it.
Common mistakes
- ✗Running
composer updateon deploy, which re-resolves and defeats the lock - ✗Adding
composer.lockto.gitignore, making builds non-reproducible - ✗Believing the lock pins only direct dependencies, not transitive ones
Follow-up questions
- →What happens on
composer installwhen the lock is out of sync withcomposer.json? - →How would you update a single package without touching the rest of the lock?
JuniorTheoryVery commonHow do namespaces prevent class-name collisions between two libraries?
How do namespaces prevent class-name collisions between two libraries?
A namespace prefixes every class, interface and function a file declares, so Acme\Log\Logger and Vendor\Log\Logger are two distinct fully-qualified names the engine never confuses. Both packages may ship a class literally called Logger: identity is the full prefixed name, not the short one.
Common mistakes
- ✗Thinking the short class name is what the engine uses for identity
- ✗Assuming a namespace loads or includes anything by itself
- ✗Believing two identical fully-qualified names can coexist in one process
Follow-up questions
- →How does an unqualified class name resolve inside a namespaced file?
- →Why must a built-in like
Exceptionbe written as\Exceptioninside a namespace?
JuniorTheoryCommonWhat is the difference between require and require-dev in composer.json?
What is the difference between require and require-dev in composer.json?
require lists packages the application needs in order to run; require-dev lists tooling needed only while developing — PHPUnit, PHPStan, fixture builders. A plain composer install installs both, but composer install --no-dev on a production deploy skips the dev block, giving a smaller vendor/ and less attack surface.
Common mistakes
- ✗Shipping dev tooling to production by omitting
--no-devon deploy - ✗Putting a package the runtime actually needs into
require-dev - ✗Assuming
--no-devchanges autoloading rather than which packages are installed
Follow-up questions
- →What breaks at runtime if a production dependency is listed under
require-dev? - →Why does
--no-devalso change the generated autoloader map?
JuniorTheoryCommonWhat is PHP-FIG, and what does a PSR actually standardise?
What is PHP-FIG, and what does a PSR actually standardise?
PHP-FIG is a group of framework and library maintainers that publishes PSRs — PHP Standard Recommendations. A PSR is not part of the language and the engine never enforces it; it is an agreed contract: a set of interfaces (PSR-3 logging, PSR-7 HTTP messages) or a convention (PSR-4 autoloading, PSR-12 style).
Common mistakes
- ✗Confusing PHP-FIG with the internals team that votes on language RFCs
- ✗Thinking the engine enforces a PSR the way it enforces a language rule
- ✗Reducing every PSR to a coding-style rule, missing the interface standards
Follow-up questions
- →Which PSRs does a typical framework implement, and which does it merely consume?
- →What would break if two libraries interpreted PSR-4 differently?
JuniorTheoryCommonHow does PSR-4 map a fully-qualified class name to a file on disk?
How does PSR-4 map a fully-qualified class name to a file on disk?
composer.json maps a namespace prefix onto a base directory — say App\ onto src/. On the first use of an undefined class the registered autoloader strips that prefix, turns the remaining separators into directory separators and appends .php, so App\Http\Kernel becomes src/Http/Kernel.php.
Common mistakes
- ✗Thinking the whole namespace must mirror the path, with no prefix stripping
- ✗Expecting the autoloader to scan directories instead of computing one path
- ✗Assuming lookups are case-insensitive, so a misnamed file still resolves
Follow-up questions
- →What does Composer do when two PSR-4 prefixes both match a class name?
- →How does a
classmapentry differ from apsr-4entry incomposer.json?
MiddleTheoryCommonHow do the ^ and ~ version constraints differ under semantic versioning?
How do the ^ and ~ version constraints differ under semantic versioning?
Both allow updates up to the next breaking change, but they draw the line differently. ^1.2.3 allows anything below 2.0.0 — minor and patch releases. ~1.2.3 allows patches only, so anything below 1.3.0. On a 0.x version ^ tightens: ^0.2.3 stays below 0.3.0.
Common mistakes
- ✗Believing
~is the looser of the two constraints - ✗Forgetting that
^on a0.xversion treats a minor bump as breaking - ✗Assuming a constraint is re-evaluated by
composer installdespite the lock
Follow-up questions
- →When would you deliberately pin an exact version instead of a range?
- →What does a package promise by following semantic versioning, and what does it not?
JuniorTheoryOccasionalWhat does a top-of-file use do, and how does it differ from use in a trait or a closure?
What does a top-of-file use do, and how does it differ from use in a trait or a closure?
A top-of-file use is a compile-time alias — it lets you write Logger for Acme\Log\Logger in that one file, and it loads nothing at all. The other two merely reuse the same token: use inside a class body composes a trait, and use ($x) after a closure signature captures an outer variable.
Common mistakes
- ✗Thinking a
useimport loads or includes the class file - ✗Expecting an alias declared in one file to apply to other files
- ✗Confusing the import
usewith trait composition or a closure capture
Follow-up questions
- →What does
use Acme\Log\Logger as AppLoggerchange about resolution? - →Why does
use ($x)capture by value, and how doesuse (&$x)differ?
MiddlePerformanceOccasionalWhat does PSR-4 autoloading cost per class, and what does --optimize-autoloader change?
What does PSR-4 autoloading cost per class, and what does --optimize-autoloader change?
Plain PSR-4 turns a class name into a path and hits the filesystem — a stat per candidate prefix — on the first use of each class, on every request. composer dump-autoload --optimize precomputes a static class-to-file map, so a lookup becomes one array read; --classmap-authoritative drops the PSR-4 fallback too.
Common mistakes
- ✗Assuming Opcache caches autoloader filesystem lookups (it caches bytecode only)
- ✗Believing the classmap is rebuilt automatically, so a deploy needs no dump-autoload
- ✗Using
--classmap-authoritativeon code that generates classes at runtime
Follow-up questions
- →Why is
--classmap-authoritativeunsafe for code that generates classes at runtime? - →How would you measure the autoloader's share of a request before optimizing it?
MiddleTheoryOccasionalHow do interoperability standards such as PSR-7 and PSR-3 decouple a library from a framework?
How do interoperability standards such as PSR-7 and PSR-3 decouple a library from a framework?
They define shared interfaces, not implementations. A library type-hinting Psr\Log\LoggerInterface or PSR-7's ServerRequestInterface runs under Monolog, Laravel or Symfony alike — the framework only has to supply a class implementing that contract. The dependency points at an interface package, never at a framework.
Common mistakes
- ✗Thinking a PSR ships an implementation rather than a set of interfaces
- ✗Type-hinting a framework's concrete class instead of the PSR interface
- ✗Believing interop is achieved by duck typing rather than by a shared contract
Follow-up questions
- →Why does
psr/logship interfaces but no logger implementation of its own? - →What does PSR-7's immutable request object change for middleware that mutates it?
SeniorDesignOccasionalA large PHP application has grown to the point where several parts — a billing engine, a notification service and a shared HTTP client — are clearly reusable and have started being copy-pasted into a second application. You are asked to restructure the codebase into an application plus a set of internal Composer packages. Describe how you would decide what becomes a package and what stays application code, how those packages would be autoloaded and required during day-to-day development, how they would be published and versioned for the second application, and what you would watch out for once the code has been split.
A large PHP application has grown to the point where several parts — a billing engine, a notification service and a shared HTTP client — are clearly reusable and have started being copy-pasted into a second application. You are asked to restructure the codebase into an application plus a set of internal Composer packages. Describe how you would decide what becomes a package and what stays application code, how those packages would be autoloaded and required during day-to-day development, how they would be published and versioned for the second application, and what you would watch out for once the code has been split.
Extract what has a stable contract and no dependency on the application; anything that knows about controllers or routes stays in the app. During development wire packages in through a path repository so edits apply instantly, and publish tagged releases to a private registry for the second app. Each package owns one namespace and one PSR-4 root.
Common mistakes
- ✗Letting a package depend back on the application, creating a circular dependency
- ✗Splitting by folder size rather than by a stable, self-contained contract
- ✗Forgetting that each package needs its own namespace and PSR-4 root
Follow-up questions
- →How does a Composer
pathrepository differ from requiring a published version? - →What is the cost of versioning every internal package with one shared tag?
SeniorPerformanceRareYour CI pipeline spends four minutes on composer install in every job. How would you cut that down?
Your CI pipeline spends four minutes on composer install in every job. How would you cut that down?
First confirm the job runs composer install and not update — with a committed lock there is nothing to re-resolve. Then cache Composer's global cache directory, keyed by the hash of composer.lock, so an unchanged lock reuses previous downloads. On deploy jobs add --no-dev --prefer-dist --no-progress.
Common mistakes
- ✗Running
composer updatein CI, re-resolving what the lock already decided - ✗Caching
vendor/without keying the cache on the lock file's hash - ✗Committing
vendor/to sidestep the install instead of caching downloads
Follow-up questions
- →Why must the cache key include the lock's hash rather than the branch name?
- →When would caching
vendor/be wrong even though it looks faster?
SeniorDesignRareYour team maintains an internal Composer package used by six applications. A widely-used method must change its signature, and the new behaviour is incompatible with the old one. The package follows semantic versioning, and every consuming application pins it with a caret constraint. Describe how you would ship this change: how you would version it, how consumers would migrate, what you would do so that no running application breaks on its next composer update, and how you would support an application that genuinely cannot migrate for another quarter.
Your team maintains an internal Composer package used by six applications. A widely-used method must change its signature, and the new behaviour is incompatible with the old one. The package follows semantic versioning, and every consuming application pins it with a caret constraint. Describe how you would ship this change: how you would version it, how consumers would migrate, what you would do so that no running application breaks on its next composer update, and how you would support an application that genuinely cannot migrate for another quarter.
Bump the major version: a caret constraint stops consumers picking it up, so nothing breaks on composer update. Before that, release a minor adding the new method beside the old one and marking the old @deprecated, so applications migrate one at a time. Keep the previous major on a maintenance branch for the laggard.
Common mistakes
- ✗Shipping an incompatible change as a minor or patch, so consumers get it silently
- ✗Removing the old method in the same release that introduces the new one
- ✗Dropping support for the previous major before every consumer has migrated
Follow-up questions
- →How would you detect that a consumer is still calling the deprecated method?
- →What changes if one consumer requires the package only transitively?