How Composer Dependency Management Works for PHP Projects
Composer manages PHP dependencies through a lock file that guarantees reproducible builds—here is the difference between install and update and why it matters.
Composer gives you a reproducible PHP build through one file, composer.lock, and running composer update in production throws that reproducibility away. Update rewrites the lock file with untested versions. Install reads it and installs exactly what you tested. Once composer.lock drifts from CI, your project runs code nobody reviewed, and the vendor directory becomes a liability. This guide covers how Composer manages dependencies and what the difference between install and update means, drawing from PHP-FIG standards and Composer’s own documentation to separate standard practice from popular convention.
How Composer Locks Reproducibility
Composer reads a composer.json file, resolves a dependency graph against Packagist as the default registry, and writes every exact version into a composer.lock file. The lock file captures the full transitive closure of requirements, down to the commit hash for each installed library. That is what makes a build reproducible across machines, CI runners, and deployment targets. The vendor directory is the output. The lock file is the source of truth.
The require key in composer.json declares the libraries your application needs to run. The require-dev key declares libraries needed only for development and testing, like PHPUnit. Both sections use version constraints that follow semantic versioning rules. Composer resolves them to a set of specific versions that satisfy every constraint in the graph. The resolver uses a SAT solver internally. When you run composer update, it recalculates that set from scratch. Running composer install reads the lock file and installs exactly those versions. It takes less time and produces the same vendor directory every time.
The autoloader Composer generates maps namespaces to directories following PSR-4, the autoloading standard defined by PHP-FIG. It also honors classmap and files entries for legacy code that does not use namespaces. Every modern PHP project assumes PSR-4 autoloading. A project without an autoloader means manual require statements, a signal that the codebase predates modern dependency management and will cost more to onboard.
Install Versus Update
The difference is simple: install honors the lock file. Update ignores it. When you run install, Composer reads composer.lock and fetches precisely the versions recorded there. It never consults the version constraints in composer.json except to verify that the lock file still satisfies them. If the lock file is missing, install behaves like update and generates one. A project without a committed lock file has no reproducible build.
Update recalculates the entire dependency graph from the version constraints in composer.json, downloads the latest permitted versions from Packagist, and rewrites composer.lock. This is the step that introduces risk. A library that released a new minor version since the last update may have changed behavior in a way that passes its own tests but breaks your application. Running update in production or as part of a routine deploy is the single most expensive Composer mistake. Update deliberately, in a development branch, with a full test suite run afterward. Commit the result. The CI pipeline should run install, never update.
Where Hosting Panels Hide The Danger
Many hosting panels and deployment tools obscure this distinction. A one-click deploy button that runs update behind the scenes is a liability. Check what your deploy process actually executes. On shared hosts with cPanel, the PHP version selector and MultiPHP INI editor define the runtime ceiling. A deploy that triggers update can pull in libraries that require a PHP version the host does not yet support, causing a white screen that logs an engine-level parse error.
What The Lock File Actually Does
The lock file pins the exact dependency graph so that every environment runs identical code. Its purpose is not speed, though install from lock is faster than update. Its purpose is not convenience, though committing the lock file makes onboarding a new developer a single-command operation. Its purpose is to guarantee that the code tested in CI is the code deployed to production. Without a lock file, you have a works-on-my-machine build.
The lock file records the library name, version, source reference, and a content hash for every installed item. It also records the PHP version constraint, the minimum stability flag, and any abandoned package flag warnings that Packagist returned at resolve time. When you run install, Composer validates the lock file hash against the composer.json content hash. If they diverge, it warns that the lock file is out of date and suggests running update. Heed that warning. A lock file that no longer matches its composer.json is a contract one party has already breached.
Merge Conflicts And The Temptation To Delete
In teams, the lock file is a source of merge conflicts. Two developers adding different libraries and running update will produce conflicting lock files. Accept one side, then run update again to regenerate the merged graph. This is tedious but safe. Deleting the lock file and running update to avoid the conflict discards every version pin and rebuilds from scratch. That can pull in a dozen changed items nobody reviewed.
PHP Dependency Management Basics
Composer is a per-project tool, not a system-level manager. It installs libraries into a vendor directory inside your project, and each project has its own set of dependencies with its own lock file. Two projects on the same server can require different versions of the same library without conflict.
The entry point is a composer.json file at the project root. It declares at minimum a project name, a description, and a require block with version constraints. The autoloader section maps the project’s own namespaces to directories so that your application code loads alongside vendor libraries. A minimal composer.json for a new PHP project needs only the require key and an autoload PSR-4 mapping. Composer resolves and installs everything else.
Composer is not a framework and does not prescribe a directory structure. It implements PSR-4 autoloading, resolves dependency graphs, and generates an autoloader. Frameworks like Laravel and Symfony use Composer as their foundation, and many of Symfony’s components are dependencies of Laravel itself. Understanding Composer means understanding the layer every modern PHP project sits on.
Packagist And The Private Alternative
Packagist is the default registry for Composer. It is a public catalogue of PHP libraries, each identified by a vendor name and library name separated by a slash. When Composer resolves dependencies, it queries Packagist for the available versions of each item and their own constraints. Packagist download counts and abandoned package flag indicators are the closest thing the PHP ecosystem has to a market-share signal.
A listing on Packagist is not necessarily maintained. The real signal of maintenance is the date of the last merge of a nontrivial pull request from a non-maintainer. Libraries with zero merged community PRs in 12 months are effectively abandoned regardless of the badge. Packagist shows the latest version, the PHP version constraint, and a list of dependents. Use that list to judge whether something is a leaf dependency with limited blast radius or a core library that many others chain through.
Private Registries
Packagist also supports private libraries through authentication and third-party repository endpoints. A company that maintains internal libraries can host them on a private Satis or Private Packagist instance and configure Composer to resolve from both the public registry and the private one. The composer.json repositories key defines additional sources, and the lock file records which repository each item came from. This is how a South Florida agency with a shared internal library of compliance utilities can distribute them across client projects without publishing to the public Internet.
| Scenario | Command To Use | Risk If Wrong Command Used |
|---|---|---|
| Deploy to production | composer install | Lock file rewritten with untested versions; production runs unreviewed code |
| CI pipeline build step | composer install | CI tests a dependency graph that differs from production; false green builds |
| New developer onboarding | composer install | Developer runs update and commits a changed lock file before any feature work |
| Add a new library | composer require then composer update | Running require without update leaves lock file stale; running update alone recalculates everything |
| Upgrade an existing library to latest permitted version | composer update vendor/package | Running a full update instead of a targeted one pulls unrelated changes |
| Lock file deleted or missing | composer install (auto-generates lock file) | Regenerated lock file pins versions that were never tested together in this combination |
Version Constraints And The Stability Trap
Composer version constraints use semantic versioning rules: a caret (^) allows minor and patch updates, a tilde (~) allows only patch updates, and an exact version pins to a single release. The constraint you choose determines how much room Composer has when resolving a graph. A project that uses caret constraints on every dependency and runs update without review is allowing every library to drift to its latest compatible version simultaneously. That is not a controlled upgrade. It is a rebuild of the entire graph.
The minimum stability setting in composer.json controls whether Composer will consider alpha, beta, or release-candidate versions during resolution. The default is stable. Lowering it to dev or alpha because you need a feature from an unreleased version opens the entire graph to unstable code, not just the one you targeted. Use the stability flag on a per-library basis through the require block’s inline stability modifier rather than lowering the global minimum. A project with global minimum stability set to dev is running unlabeled code and will experience breakage on the next update.
Failure Modes That Corrupt The Vendor Directory
The most expensive Composer failure is memory exhaustion. Running update on a large dependency graph without raising the PHP memory limit kills the process after resolving a fraction of the libraries. The vendor directory is left in an inconsistent state, and the lock file is either missing or partial. Raise the memory limit to at least 1.5 GB for large projects, and monitor the peak memory usage reported by Composer at the end of a successful run to know your actual requirement.
Production Xdebug is a configuration failure, not a Composer failure, but it compounds Composer problems. Deploying a container or server image with Xdebug enabled means every request carries debugging overhead, and the opcache may be disabled as a side effect. The install step itself runs slower, and the autoloader generation step consumes more memory. Check phpinfo() on the production host and confirm that Xdebug is disabled and opcache is enabled with a cache size large enough for the codebase.
The .env-in-production problem affects Composer indirectly. Relying on .env files in production instead of real environment variables means the framework parses the file on every request if config caching is not enabled. A deployment that loses the .env file loses database credentials and API keys. Composer scripts that run post-install or post-update can reference environment variables, and a missing .env file breaks those scripts silently. Cache the config in production and use real environment variables, not a file that can be deleted by a deployment artifact cleanup step.
The Reproducible Build Checklist
A reproducible PHP build requires five things. First, the composer.lock file is committed to version control. Second, the deployment process runs composer install, never update. Third, the production environment’s PHP version matches the platform requirement in composer.json. Fourth, the autoloader is generated with the optimize-autoloader flag enabled, which produces a classmap that speeds up class discovery and avoids filesystem stat calls on every request. Fifth, the vendor directory is either committed or generated during deploy from a cached Composer state, but never generated fresh from an update on the production host.
The Optimize Flag
The optimize-autoloader flag in composer.json’s config section is a one-line change that reduces autoloading overhead in production. It generates a classmap that maps every class in every library to its file location, eliminating the filesystem traversal that PSR-4 autoloading normally requires. On a project with a large dependency graph, the difference is measurable: a cold request without an optimized autoloader can spend 15 to 30 milliseconds in autoloading alone. An optimized classmap reduces that to under 2 milliseconds.
The One Check That Prevents Regressions
The single thing that most often goes wrong is a deploy script that runs composer update by default because a tutorial or hosting provider’s one-click installer configured it that way. Check the deploy log. If it shows dependency resolution steps rather than a message that the lock file is being read, your build is not reproducible. Change the command to composer install and verify that the lock file hash matches after deploy. This one check prevents the class of production incidents where an update introduces a regression that no human reviewed.
Common Questions
What happens if I delete the composer.lock file and run install?
Composer behaves as if you ran update: it resolves the entire dependency graph from the constraints in composer.json and writes a new lock file. The new lock file pins versions that may differ from the ones that were previously installed and tested. Never delete the lock file to solve a merge conflict. Accept one side of the conflict and run update deliberately in a development branch.
Should I commit the vendor directory to version control?
No. The vendor directory is generated output. Commiting it bloats the repository and creates merge conflicts on every dependency change. The lock file is the artifact that guarantees reproducibility. The one exception is a deployment target that cannot run Composer, but in that case you should generate the vendor directory in a build step and deploy it as an artifact, not commit it.
How do I pin a library to an exact version?
Use an exact version constraint in composer.json, such as "vendor/package": "2.3.1". This prevents Composer from installing any other version during update. Beware that this also blocks updates to that library’s own dependencies if they require a newer version, which can create an unresolvable graph. Use exact constraints sparingly and only for libraries with known compatibility problems.
What does the abandoned flag on Packagist mean?
It means the maintainer has marked the library as abandoned and optionally suggested a replacement. Composer warns about abandoned items during install and update. A listing without the flag can still be effectively abandoned if it has received no merged community pull requests in 12 months. Check the commit history and issue tracker, not just the Packagist badge.
Why does Composer run out of memory during update?
Dependency resolution is a SAT problem whose memory requirement grows with the size of the dependency graph. Large projects with many transitive dependencies can require 1.5 GB or more of PHP memory. Raise the memory_limit in php.ini or pass -d memory_limit=-1 to the Composer command for unlimited memory during resolution. Monitor the peak memory usage reported at the end of a successful run.
What is the difference between PSR-4 and the Composer classmap autoloader?
PSR-4 maps namespaces to directories at runtime, which requires filesystem stat calls on every autoloaded class. The classmap autoloader precomputes every class-to-file mapping and stores it in an array, which eliminates those stat calls. Use the optimize-autoloader config flag to generate the classmap for production. PSR-4 is the standard; the classmap is the optimized implementation generated by Composer.
Can I use Composer without Packagist?
Yes. The repositories key in composer.json can point to private Satis instances, Private Packagist, or direct VCS repositories. A project can resolve dependencies entirely from private sources. The lock file records which repository each library came from, so the source is reproducible. Many enterprise environments use a private mirror of Packagist to control which libraries are available and to cache downloads.