Migrate to RunxBuild and earn up to $50 in hosting credit on your first deposit.

Calculate your savings
unxBuild
Back to Blog Explainer

PHP Composer: Dependency Management, and the Two Files That Matter

Sean

Platform Writer

Aug 17, 2026
9 min read

Composer is the dependency manager for PHP. You declare what your project needs in composer.json, Composer resolves the versions and records the exact results in composer.lock, and everyone who runs composer install gets byte-identical dependencies. That second file is the one that makes builds reproducible, and it is the one people misunderstand.

PHP Composer: Dependency Management, and the Two Files That Matter

Before Composer, adding a library to a PHP project meant downloading a zip, dropping it in a folder, writing your own include statements, and repeating the process by hand whenever it updated — with transitive dependencies handled entirely by you. Composer replaced all of that with two commands and generated an autoloader for free, which is why it went from a community project to a hard requirement for essentially every modern PHP framework.

Table of contents

The two files, and the difference between install and update

composer.json is your declaration of intent: which packages, at which version ranges.

{
  "require": {
    "php": ">=8.2",
    "guzzlehttp/guzzle": "^7.8",
    "monolog/monolog": "^3.5"
  },
  "require-dev": {
    "phpunit/phpunit": "^11.0"
  },
  "autoload": {
    "psr-4": { "App\\": "src/" }
  }
}

composer.lock is the resolved result: the exact version and commit of every package, including transitive dependencies you never named. It is generated, and it should absolutely be committed to version control.

The two commands do genuinely different things and mixing them up is the most common Composer mistake:

  • composer install reads the lock file and installs exactly those versions. If there is no lock file, it resolves and creates one. This is what runs in CI and in deployment.
  • composer update ignores the current lock, re-resolves everything within the ranges in composer.json, and writes a new lock file. This is a deliberate act performed by a developer, reviewed, tested, and committed.

Running composer update in a deploy pipeline defeats the entire purpose of the lock file: every deploy resolves whatever versions are current that day, so production runs code nobody tested. If you take one thing from this page, make it that deployment runs install, never update.

Version constraints

The syntax is compact and the two common operators behave differently in an important way.

  • ^7.8 — caret. Allows anything up to but not including 8.0. Under semantic versioning that means bug fixes and new features, no breaking changes. This is the sensible default.
  • ~7.8.1 — tilde. Allows 7.8.x but not 7.9. More conservative: patches only.
  • 7.8.3 — exact. No updates at all. Use sparingly; it blocks security patches too.
  • >=7.0 <8.0 — an explicit range.
  • * — anything. Do not do this.

Caret is the right default for libraries that follow semver, which most of the popular ones do. It gets you security fixes and improvements without breaking changes, and it keeps the range meaningful.

One caveat worth knowing: for packages below 1.0, caret behaves more conservatively. ^0.3.1 allows 0.3.x but not 0.4, because pre-1.0 packages are understood to break on minor bumps. That is deliberate and it surprises people who expect symmetry.

Also declare the PHP version your project needs. "php": ">=8.2" makes Composer refuse to install on an older runtime rather than letting you discover the incompatibility at runtime in production.

The autoloader you get for free

Composer generates an autoloader, and it is arguably as valuable as the dependency resolution.

<?php
require __DIR__ . '/vendor/autoload.php';

use GuzzleHttp\Client;
use App\Services\PaymentProcessor;

$client = new Client();

One require line and every class from every package is available, loaded on demand. No manual includes, no ordering problems, no file that grows to two hundred require statements.

Your own code joins the same system through the autoload section. PSR-4 maps a namespace prefix to a directory:

{
  "autoload": {
    "psr-4": { "App\\": "src/" },
    "files": [ "src/helpers.php" ]
  },
  "autoload-dev": {
    "psr-4": { "Tests\\": "tests/" }
  }
}

With that, App\Services\PaymentProcessor is expected at src/Services/PaymentProcessor.php. The mapping is mechanical, which is the point — no configuration per class.

After changing the autoload section, run composer dump-autoload. Forgetting that is the cause of the class not found error that appears immediately after adding a new namespace, and it is a two-second fix that people spend twenty minutes on.

Deployment

The production install differs from the development one in three ways that all matter.

composer install --no-dev --optimize-autoloader --no-interaction --prefer-dist
  • --no-dev skips require-dev packages. PHPUnit and debugging tools have no business on a production server, and they are attack surface as well as weight.
  • --optimize-autoloader converts PSR-4 rules into a static classmap. It removes filesystem checks on every class load and is a measurable improvement on a request-per-request runtime.
  • --no-interaction stops it waiting for a prompt in a non-interactive pipeline.
  • --prefer-dist fetches package archives rather than cloning repositories, which is faster.

Whether to commit vendor/ is a genuine debate. Committing it makes deploys not depend on Packagist being available and removes a build step; it also bloats the repository and makes diffs unreadable. The more common answer today is: do not commit it, run composer install in the build, and keep the lock file committed so the result is deterministic anyway.

Either way, composer install in the build must use the committed lock file. That is the whole guarantee.

Auditing and keeping things current

Composer has a built-in security check that reads the community advisory database.

composer audit
composer outdated
composer outdated --direct
composer why vendor/package
composer why-not vendor/package 2.0

composer audit reports known vulnerabilities in your installed versions. Run it in CI and fail the build on findings — it costs nothing and it catches the class of problem that otherwise gets discovered by someone else.

composer outdated --direct limits the report to packages you actually declared, rather than the full transitive tree, which makes it readable.

The two why commands are the underrated ones. composer why explains which package pulled in a dependency you did not ask for. composer why-not explains why a version you want cannot be installed, which turns an opaque resolution failure into a specific conflicting constraint.

Update deliberately: run composer update locally, read what changed in the lock file diff, run the test suite, commit both files together. Updating one package at a time — composer update vendor/package — keeps the blast radius small when something breaks.

The errors you will meet

  • Your requirements could not be resolved. Two packages want incompatible versions of a third. composer why-not names the conflict. Sometimes the answer is that one of them is simply not ready for the version you want.
  • Allowed memory size exhausted. Resolution is memory-hungry on large projects. COMPOSER_MEMORY_LIMIT=-1 composer update removes the cap.
  • Class not found immediately after adding a file. Run composer dump-autoload. Almost always this.
  • The lock file is not up to date with the latest changes in composer.json. Someone edited the json without running update. Run composer update --lock if only metadata changed, or a proper update if requirements did.
  • Composer is out of date. composer self-update. Version 1 and version 2 differ enough that some errors are simply the old version.
  • Package requires ext-something. A PHP extension is missing from the runtime, not a Composer problem. Install the extension.

How this fits the rest of the stack

The deployment section above only holds if the environment running composer install is the same one serving requests — same PHP version, same extensions, same lock file. That is exactly what a build from a repository gives you: the install runs as part of the build, the log is attached to the deploy that produced it, and the previous deploy stays available when an update turns out to be the problem. PHP applications deploy from a GitHub repository on RunxBuild with build logs, a live route, environment variables, and rollback — deploying from GitHub on RunxBuild covers connecting a repository. When you are sizing the app plus a managed MySQL or Postgres beside it, the RunxBuild hosting calculator shows each as a separate figure.

Useful related references:

FAQ

What is the difference between composer install and composer update?

install reads composer.lock and installs exactly those versions — this is what runs in CI and deployment. update ignores the lock, re-resolves everything within the ranges in composer.json, and writes a new lock file. Running update during deployment means production runs versions nobody tested.

Should I commit composer.lock to git?

Yes, always. It records the exact version of every package including transitive dependencies, which is what makes composer install produce identical results for every developer and every deployment. Committing it is the entire point of having it.

What does the caret in ^7.8 mean?

It allows any version up to but not including the next major — so 7.8 through 7.99, but not 8.0. Under semantic versioning that means bug fixes and features without breaking changes. Note that below 1.0 it is more conservative: ^0.3.1 allows 0.3.x but not 0.4.

Why do I get class not found after adding a new file?

The autoloader classmap is stale. Run composer dump-autoload. This is needed after changing the autoload section in composer.json or adding a new namespace, and it accounts for the large majority of unexpected class-not-found errors.

How do I check my PHP dependencies for vulnerabilities?

Run composer audit, which checks your installed versions against the community security advisory database. Run it in CI and fail the build on findings. Pair it with composer outdated --direct to see which of your declared packages have newer releases.

#php composer#composer#php dependencies#autoloading#packagist