Published on 08/25/2026
I use Composer very regularly in my day-to-day, however, prior to my current role I only used it occasionally. Earlier on, when it came to dependency management (between the install, update, update vendor/package, and require commands), I often found myself needing to look up which was the best option for any given scenario.
While not an in-depth guide by any means (I’ll mainly only be covering differences between install and the various flavors of update, and composer require), cleaning my notes up into something reusable felt worth sharing.
Before we get into the aforementioned Composer commands, we’ll need to have a basic understanding of semantic versioning - more commonly referred to by its short name, “semver”. That is a topic worth it’s own article but I’ll give a quick overview, focusing predominantly on the caret operator and keeping within a desired version range.
The caret (^) operator means "compatible with," based on semantic versioning. The leftmost non-zero digit acts as the upper bound, and Composer will never cross it.
^1.22.4Range: >= 1.22.4 and < 2.0.0
The floor is locked at 1.22.4. Composer won't install anything below that, but it will accept any newer minor or patch release within the 1.x line.
| Version | Allowed? |
|---|---|
1.22.3 | ❌ Below floor |
1.22.4 | ✅ Exact floor |
1.22.9 | ✅ Higher patch |
1.23.0 | ✅ Higher minor |
1.99.99 | ✅ Still within 1.x |
2.0.0 | ❌ Breaks upper bound |
^1.22Range: >= 1.22.0 and < 2.0.0
With no patch version specified, the floor drops to 1.22.0. The upper bound behaves the same as above.
| Version | Allowed? |
|---|---|
1.21.9 | ❌ Below floor |
1.22.0 | ✅ Exact floor |
1.22.3 | ✅ Higher patch |
1.23.0 | ✅ Higher minor |
2.0.0 | ❌ Breaks upper bound |
So the only practical difference between the two is the minimum version floor:
^1.22.4 sets the floor at 1.22.4. Use this when a specific patch fixed a bug, patched a security issue, or introduced something your code relies on.^1.22 sets the floor at 1.22.0. Use this when any release from that minor version onward is fine.Worth remembering: composer.lock freezes the exact resolved version regardless of what the constraint says. The constraint only comes into play when resolving a fresh install or running composer update.
composer install, update, and update vendor/package commandsThese three commands can get used somewhat interchangeably if you're not paying attention, but they do meaningfully different things.
composer installReads composer.lock and installs exactly the versions frozen there, ignoring composer.json for version resolution entirely. If a package is missing from the lock file, it gets added, but an already-locked version never changes.
Use this for deploying to production or setting up a project from an existing repo. It's what guarantees a reproducible environment.
composer update (no arguments)Re-resolves every package against composer.json, finding the latest version each constraint allows, and rewrites composer.lock across the board.
I'd use this with caution. It can pull in unexpected updates to dozens of transitive dependencies you never intended to touch. It's better suited for a deliberate, full dependency audit than everyday use.
composer update vendor/package-name (my default for day to day)Re-resolves only the package you name, against the constraint already in composer.json, and rewrites just that entry in composer.lock. Everything else stays frozen.
You can update multiple packages in one command too:
A few flags worth knowing:
| Flag | Effect |
|---|---|
--dry-run | Shows what would change without applying it, good for sanity-checking |
--with-dependencies | Also updates the dependencies of the target package |
--no-dev | Excludes require-dev packages, useful for production |
My safe workflow when I'm editing a constraint by hand looks like this:
The way I keep these straight:
| Command | What it means |
|---|---|
composer install | "Give me exactly what the lock file says." |
composer update vendor/pkg | "Re-resolve this package against my constraint." |
composer update | "Re-resolve everything." (use with caution) |
And always commit both composer.json and composer.lock, so the team and the deploy pipeline get deterministic installs via composer install.
composer require commandcomposer require adds or changes a package declaration and installs it in one step. It edits composer.json for you, then resolves and installs.
That single command does three things atomically:
composer.jsoncomposer.lock and installs itYou can also pin a specific constraint:
composer require vs composer update vendor/packageThe real difference between the two comes down to who edits composer.json:
| Command | Who edits composer.json? | What it does |
|---|---|---|
composer require | Composer does it for you | Adds/changes the constraint and installs |
composer update vendor/pkg | You edit it manually first | Resolves the constraint you already wrote |
Use composer require when you're adding a brand new package and want Composer to figure out a sensible constraint for you. Reach for composer update vendor/package when you've already edited the constraint by hand and want finer control over what ends up written in composer.json.
Running require against an already-installed package also works for bumping its constraint:
That's functionally equivalent to manually editing composer.json and running composer update monolog/monolog, just condensed into one command. I still lean toward the manual edit plus update approach in most cases, since it's more explicit and easier to review in a pull request diff.
The main gottcha to be aware of is that composer require can silently overwrite a constraint you set by hand
This one has caught me before, so it's worth calling out on its own. If you manually add a package and constraint to composer.json, then later run composer require vendor/package without specifying a constraint, Composer will overwrite what you wrote with its own resolved version. Silently.
Say you manually write this in composer.json:
Then run:
Composer may rewrite it to:
Your ^1.22.4 constraint is just gone.
There are two safe ways around this. Either use composer update vendor/package, which respects and keeps the constraint you already wrote and simply resolves against it:
Or specify the constraint explicitly when you require it, which tells Composer exactly what to write and preserves your intent:
That’s all for today. The main takeaway from here is, once you've manually edited composer.json, use composer update vendor/package to action that change. Running composer require without a constraint on a package that's already there is essentially telling Composer "forget what I wrote, pick a version for me."
---
Category: PHP
Tags: composer, dependency-management, workflow