DWR.IO

Managing PHP Packages with Composer

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.

Semver and the caret operator

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.4

Range: >= 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.

VersionAllowed?
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.22

Range: >= 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.

VersionAllowed?
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:

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.

The composer install, update, and update vendor/package commands

These three commands can get used somewhat interchangeably if you're not paying attention, but they do meaningfully different things.

composer install

Reads 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.

composer update monolog/monolog

You can update multiple packages in one command too:

composer update monolog/monolog guzzlehttp/guzzle

A few flags worth knowing:

FlagEffect
--dry-runShows what would change without applying it, good for sanity-checking
--with-dependenciesAlso updates the dependencies of the target package
--no-devExcludes require-dev packages, useful for production

My safe workflow when I'm editing a constraint by hand looks like this:

# 1. Manually edit the constraint in composer.json first

# 2. Preview the change
composer update vendor/package-name --dry-run

# 3. Apply the change
composer update vendor/package-name

The way I keep these straight:

CommandWhat 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.

The composer require command

composer require adds or changes a package declaration and installs it in one step. It edits composer.json for you, then resolves and installs.

composer require monolog/monolog

That single command does three things atomically:

  1. Finds the latest compatible version of the package
  2. Writes the dependency into composer.json
  3. Updates composer.lock and installs it

You can also pin a specific constraint:

composer require monolog/monolog:^2.0

composer require vs composer update vendor/package

The real difference between the two comes down to who edits composer.json:

CommandWho edits composer.json?What it does
composer requireComposer does it for youAdds/changes the constraint and installs
composer update vendor/pkgYou edit it manually firstResolves 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:

# Bumps the constraint and updates the package in one shot
composer require monolog/monolog:^3.0

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.

Things to look out for

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:

"monolog/monolog": "^1.22.4"

Then run:

composer require monolog/monolog

Composer may rewrite it to:

"monolog/monolog": "^3.0"

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:

composer update monolog/monolog

Or specify the constraint explicitly when you require it, which tells Composer exactly what to write and preserves your intent:

composer require monolog/monolog:^1.22.4

Wrapping up

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

← Back to all notes