composer require: The One Command That Edits Two Files at Once
You need monolog/monolog in the project. The wrong move is opening composer.json by hand and running composer install — the manifest edit invalidates the lock hash, and install's job is to obey the lock, not to reconcile it. The right move is one command:
composer require monolog/monolog:^3.0
composer require performs the whole transaction: it resolves the new package (and any of its dependencies) against everything already locked, writes the constraint into composer.json and the resolved version into composer.lock together, copies the code into vendor/, and regenerates the autoloader. Manifest and lock never disagree, because both files are written in the same operation. That atomicity is the practical reason to prefer require/remove over editing JSON yourself.
The constraint you pass inline becomes the permanent declaration. monolog/monolog:^3.0 records "monolog/monolog": "^3.0" in require — not the resolved 3.9.1. This distinction is the whole game: the manifest stores your policy, the lock stores the current reality. Omit the constraint (composer require monolog/monolog) and Composer asks interactively, defaulting to ^ plus the latest version — fine at a terminal, a hang in CI.
Dependencies you need only while developing go in require-dev:
composer require --dev phpunit/phpunit:^10.5
composer require --dev --no-interaction friendsofphp/php-cs-fixer:^3.0
require-dev entries install by default on every machine and are skipped with --no-dev on production — test tooling should never ship to a server that does not run tests.
Removal is symmetric and just as atomic:
composer remove monolog/monolog # drops it from composer.json AND the lock
echo $? # non-zero on failure — CI can gate on it
The code leaves vendor/, the autoloader regenerates, and any class your code still references now fails with Class "Monolog\Logger" not found — which is the correct outcome: removal exposes real usage instead of leaving dead code half-alive.
composer require can also move packages you did not name. If the new package needs psr/log:^3.0 while your project holds ^1.0, resolution fails rather than quietly breaking your declared constraint: Root composer.json requires psr/log ^1.0 ... conflicts with psr/log 3.x. Composer never silently makes a breaking change to a package you declared — it names the constraint for you to review. Resolving the conflict is editorial: check what your code touches in that API, widen the range, and let require do the mechanical part.
Every such command updates vendor/composer/installed.json — the machine-readable inventory of what is present, transitive dependencies included. Humans read it through Composer:
composer show # everything installed, with versions
composer show monolog/monolog # one package: version, license, its require block
What require will not do is upgrade an existing entry's range. Running composer require monolog/monolog:^3.0 when ^2.0 is already declared rewrites the constraint (it is an edit, not a no-op) — but bumping versions inside an existing range, or upgrading packages the manifest already allows, is composer update's territory. "How do you add a dependency in Composer?" sounds like a trivia question; the answer worth giving names all four effects — manifest, lock, vendor, autoloader — and states that the constraint you type is the policy the team will live under.
composer update: A Team Decision Disguised as a Local Command
composer install obeys the lock. composer update rewrites it — every package in the manifest gets re-resolved to the newest version satisfying its constraint, the lock file is regenerated, and your machine now runs a tree nobody else has. That is why an innocent composer update on a Friday afternoon is a reviewable change and not a housekeeping chore.
composer update # re-resolve EVERYTHING — the widest blast radius
composer update monolog/monolog # just this package (and its needed deps)
composer update monolog/monolog -W # same, plus allow its dependencies to move
The scoped form matters. composer update vendor/pkg updates that package and whatever must move with it; -W (--with-all-dependencies) widens it to shared transitive dependencies that otherwise block the upgrade. Without a target, one forgotten composer update can drag twenty libraries across major versions in a single diff.
The workflow consequence is absolute: CI and production run composer install, never update. Install replays the committed lock; update invents a new tree. A pipeline that updates is a pipeline where the same commit builds differently on Tuesday than on Monday.
Two operational details round out the command. composer update vendor/pkg --dry-run prints exactly what would change — versions in, versions out — without touching the lock, which makes the preview itself reviewable in a pull request. And because the lock is a single JSON file, two simultaneous dependency pull requests collide in it; the resolution is never to hand-merge the JSON. Take one side and re-run the scoped update so the resolver regenerates a consistent tree:
git checkout --ours composer.lock # or --theirs
composer update pkg-a pkg-b -W
The regenerated lock is the union of both intents, computed by Composer rather than by a text editor guessing at intertwined metadata blocks.
The team habit that makes this scale is reviewing lock changes like code. The lock is machine-generated, but its diff is readable: package names, versions, and references. A pull request that bumps symfony/http-foundation from v6.4.3 to v7.0.0 is visible in the lock diff even when composer.json is untouched — and a major bump sneaking in unnoticed is exactly the incident lock review prevents.
Keeping the manifest honest over time is a scheduled task, not an accident:
composer outdated --direct # which direct dependencies have newer versions?
composer outdated --minor-only # safe-looking bumps within your ranges
The limits of the safety net are worth stating in interviews: semantic versioning is a promise, and packages occasionally break promises inside a minor range (a mis-tagged release, a behavioral change with a patch bump). The lock plus review slows the blast radius down to a deliberate diff; it cannot make upstream releases correct. When a minor bump does break you, composer require vendor/pkg:the-old-exact-version pins the escape while you file the issue.
"What is the difference between composer install and composer update?" is the single most asked Composer question in PHP interviews. One sentence — install replays the lock, update re-resolves and rewrites it — plus the CI rule (install only) and the team-decision framing is a complete answer.
Reading Constraint Syntax: ^, ~, Hyphens, Wildcards, and the 0.x Trap
Version constraints are where "upgrade freely" and "pin exactly" get expressed precisely. Composer uses caret (^), tilde (~), hyphen ranges, wildcards, and exact versions — and the operator interacts with the version's own shape in ways even experienced developers misread.
| Constraint | Accepts | Rejects |
|---|---|---|
^2.1.3 |
>=2.1.3 <3.0.0 |
2.1.2, 3.0.0 |
^2.1 |
>=2.1.0 <3.0.0 |
2.0.9, 3.0.0 |
~2.1.3 |
>=2.1.3 <2.2.0 |
2.1.2, 2.2.0 |
~2.1 |
>=2.1.0 <3.0.0 |
2.0.9, 3.0.0 |
1.2.3 |
exactly 1.2.3 |
anything else |
1.2.* |
>=1.2.0 <1.3.0 |
1.3.0 |
1.2 - 1.4.5 |
>=1.2.0 <=1.4.5 |
1.4.6 |
The rule behind the table: ^ locks the leftmost digit that can change — ^2.1.3 allows anything below 3.0.0, because the major version is the boundary. ~ locks everything left of the last digit you wrote — with three digits it behaves like a patch-range caret (~2.1.3 → <2.2.0), with two digits it behaves exactly like ^2.1. When in doubt, ^ is what teams reach for: minor and patch releases in, major releases out.
The trap is the 0.x world. By semantic-versioning convention, 0.x majors are unstable: ^0.2.3 means >=0.2.3 <0.3.0 — a 0.3.0 release is treated as breaking, because pre-1.0 packages use the minor digit as their breaking-change boundary. A library pinned ^0.4 therefore gets no 0.5 upgrades at all, and teams staring at composer outdated often misread a working constraint as broken. The fix is knowing the convention, not fighting it.
Pinning exact versions ("monolog/monolog": "3.9.1") has a place — shared libraries that must not drift, or an emergency pin after a bad release — but for applications it forfeits patch and security fixes unless someone bumps the pin by hand every week. The pairing that works in practice is a caret range in the manifest plus the lock file: policy stays flexible, reality stays pinned until a human deliberately moves it.
Stability is the other axis. Stable releases (1.2.3, v3.0.0) are installable by default; betas, release candidates, and dev branches are not, because minimum-stability defaults to stable. Opting in is explicit and scoped:
composer require acme/feature-flag:1.1.x-dev@dev # one package, dev branch
composer require acme/sdk:^2.0@beta # accept betas inside this range
The @dev (or @beta, @RC) suffix is a per-package stability override. Widening the project instead — minimum-stability: dev with prefer-stable: true — tells Composer "allow anything, but prefer stable when a stable version satisfies the range"; that pair is the standard shape for actively developed applications, while libraries should keep the default stable so their consumers are not forced into risk.
Two extensions of the syntax appear in real manifests. A wildcard inside a range (>=1.2 <2.0.*) is legal but rare — an explicit caret reads better. And packages can alias their own dev branches: a library on main may declare "branch-alias": {"dev-main": "2.1.x-dev"} so consumers can depend on dev-main while resolution reasons about it as a would-be 2.1 release. That alias is why composer require vendor/pkg:dev-main behaves consistently in dependency solving — and why an untagged branch is still riskier than any tagged release, whatever the alias claims.
For interviews, practice the translation in both directions: given ~1.4.2, say >=1.4.2 <1.5.0; given "I want everything below 2.0 except nothing, at least 1.8", say ^1.8. The constraint language is small; knowing that 0.x and stability flags sit outside it is the depth signal.
Quiz
1. What files does `composer require vendor/pkg:^2.0` modify?
2. Your CI pipeline runs `composer update` on every build. What is the consequence?
3. A dependency is declared as `^0.2.3`. Which version will Composer accept?
4. You want to experiment with an unreleased branch of one package without weakening stability for the whole project. What do you do?
Flashcards
Question
What does `composer require vendor/pkg:^2.0` actually do?
Click to reveal answer
Answer
Resolves the package and its dependencies against what is locked, writes the constraint into composer.json and the exact resolved versions into composer.lock together, installs into vendor/, and regenerates the autoloader. The recorded constraint is your policy (^2.0), not the resolved version (2.4.1).
Question
composer install vs composer update — and where each belongs
Click to reveal answer
Answer
install replays composer.lock exactly (running it after a manifest edit only warns about lock drift). update re-resolves every range and rewrites the lock — a team-wide decision. Rule: developers/CI/production run install; updates happen in a reviewed pull request whose lock diff is read like code.
Question
Expand these constraints: ^2.1.3 · ~2.1.3 · ^0.2.3 · 1.2.*
Click to reveal answer
Answer
^2.1.3 → >=2.1.3 <3.0.0 (caret locks the leftmost digit written). ~2.1.3 → >=2.1.3 <2.2.0 (tilde locks everything left of the last digit). ^0.2.3 → >=0.2.3 <0.3.0 (0.x minors are breaking boundaries). 1.2.* → >=1.2.0 <1.3.0.
Question
How do you accept a beta or dev branch for one package only?
Click to reveal answer
Answer
Per-package stability flags: `composer require acme/pkg:^2.0@beta` or `acme/pkg:1.1.x-dev@dev`. The project-wide alternative is minimum-stability: dev plus prefer-stable: true — that exposes every dependency to pre-releases, so libraries should stay on the stable default.
Question
How do you upgrade one package safely, and how do you see what is outdated?
Click to reveal answer
Answer
`composer update vendor/pkg` re-resolves just that package; add -W (--with-all-dependencies) when shared transitive deps block it; bare `composer update` moves everything. `composer outdated --direct` lists outdated direct dependencies, `--minor-only` restricts to in-range bumps — then commit the lock diff for review.
Revision Notes
Key Takeaways
- 1. composer require/remove are transactions: the constraint lands in composer.json and the resolved versions in composer.lock together, so the files never drift
- 2. The manifest stores policy (^3.0), the lock stores reality (3.9.1) — install obeys the lock, update rewrites it, and CI must only ever install
- 3. ^ locks the leftmost version digit you wrote; tilde locks everything left of the last digit; for 0.x packages the caret locks the minor because pre-1.0 minors are breaking boundaries
- 4. Stability is opt-in per package with @dev/@beta suffixes — project-wide minimum-stability: dev is a blunt instrument best paired with prefer-stable: true
- 5. Lock diffs are reviewable: a major bump hiding in a pull request shows up in composer.lock even when composer.json is untouched
Interview Tips
- • "Difference between composer install and composer update?" — one crisp sentence (replay vs re-resolve), then the CI rule and why an updating pipeline builds the same commit differently on different days
- • "How do you add a dependency?" — name all four effects (manifest, lock, vendor, autoloader) and say the inline constraint becomes the team's policy, not the resolved version
- • "What does ^0.2.3 allow?" — >=0.2.3 <0.3.0; explaining that 0.x uses the minor digit as the breaking boundary is the differentiator
- • "A minor upgrade broke production. What now?" — pin the last good exact version to recover, then check the upstream changelog/release notes; acknowledge semver is a promise that can be broken and the lock+review process limits blast radius
Cheat Sheet
Installing and Updating Packages
Atomic commands (edit manifest + lock + vendor together):
composer require monolog/monolog:^3.0
composer require --dev phpunit/phpunit:^10.5
composer remove monolog/monolog
composer update monolog/monolog -W # one package (+ shared deps if -W)
install vs update: install = replay the lock (CI/production only) · update = re-resolve and rewrite the lock (reviewed PR only). Manifest edit without update → "lock file is not up to date" warning.
Constraint table:
| Written | Means |
|---|---|
^2.1.3 |
>=2.1.3 <3.0.0 |
~2.1.3 |
>=2.1.3 <2.2.0 |
^0.2.3 |
>=0.2.3 <0.3.0 (0.x minor = breaking) |
1.2.3 |
exact |
1.2.* |
>=1.2.0 <1.3.0 |
Stability: default minimum-stability is stable; scope with @dev/@beta suffixes per package; minimum-stability: dev + prefer-stable: true is the app-wide form.
Audit: composer outdated --direct / --minor-only.