Let's Connect

Tangled network of dependency lines on a dark screen representing Composer dependency conflicts

Composer dependency conflicts almost always announce themselves the same way: a wall of red ending in "Your requirements could not be resolved to an installable set of packages." The fix is not to delete composer.lock and hope. That message is a diagnostic report — it names the two constraints that disagree, and once you read which package requires what, the resolution is usually a one-line constraint bump or a targeted composer update. Here is how I work through it on a production project instead of guessing.

What does "could not be resolved to an installable set" actually mean?

Composer's solver has to find one version of every package that satisfies every constraint at the same time. When it can't, it prints the chain that boxed it in. Read past the first line into the "Problem 1" block — that is where the real conflict lives. A typical message looks like this:

composer require output
$ composer require symfony/console:^7.0

./composer.json has been updated
Running composer update symfony/console
Loading composer repositories with package information
Updating dependencies
Your requirements could not be resolved to an installable set of packages.

  Problem 1
    - Root composer.json requires symfony/console ^7.0 -> satisfiable by symfony/console[v7.0.0, ..., v7.1.6].
    - acme/legacy-pkg 1.2.0 requires symfony/console ^6.0 -> found symfony/console[v6.0.0, ..., v6.4.7] but it conflicts with your root composer.json require (^7.0).
    - acme/legacy-pkg is locked to version 1.2.0 and an update of this package was not requested.

Read down that chain and the story is plain: you asked for symfony/console ^7.0, but acme/legacy-pkg 1.2.0 requires symfony/console ^6.0, and that package is pinned in the lock file. The two ranges don't overlap, so there is no single version of symfony/console that satisfies both. Re-running composer install won't change that — you have to move one of the two constraints.

Who requires this package, and what is blocking the upgrade?

Before touching any version string, find out the shape of the dependency graph. Two built-in commands answer the only questions that matter: who pulls a package in, and what stops it from upgrading. composer why (alias of depends) walks up the tree; composer why-not (alias of prohibits) tells you which constraint blocks a target version.

diagnose the graph
# Who depends on symfony/console, and through what chain?
composer why symfony/console
composer why symfony/console --tree

# What is preventing symfony/console 7.0 from being installed?
composer why-not symfony/console 7.0

# Output points the finger directly:
#   acme/legacy-pkg  1.2.0  requires  symfony/console (^6.0)

That why-not output is the whole investigation in one line. Now you know the decision is between three concrete options: upgrade acme/legacy-pkg to a version that allows Symfony 7, loosen your own constraint back to something both can share, or remove the offending package. Guessing is over — now you are choosing.

Source code on a screen showing nested dependency constraints being traced line by line
composer why-not turns a wall of red into a single named constraint you can act on.

How do I actually fix the conflict?

Once you know the blocking constraint, the fix is one of a small, predictable set. Work through them in this order — cheapest and safest first:

  • Bump the blocker. If acme/legacy-pkg has a 1.3.0 that allows symfony/console ^7.0, upgrade it: composer require acme/legacy-pkg:^1.3. This is the clean fix and usually the right one.
  • Move a transitive dependency. When the conflicting package is not a direct require, update it along with its dependents using composer update vendor/pkg --with-all-dependencies (the -W flag). Without -W, Composer won't touch root-level dependencies and you get the same wall of red.
  • Loosen your own constraint. If you over-pinned (^7.0 when ^6.4 || ^7.0 would do), widen it. Tight constraints are the most common self-inflicted cause of conflicts.
  • Remove or replace the package. A genuinely abandoned library that hard-pins an old major is dead weight. Drop it or swap it before it pins your whole tree to the past.
targeted resolution
# Preferred: bump the package that holds the old constraint
composer require acme/legacy-pkg:^1.3 -W

# Move a transitive dependency without churning the whole lock file
composer update symfony/console --with-all-dependencies

# See exactly what WOULD change before committing to it
composer update symfony/console -W --dry-run

Always run --dry-run first on anything beyond a single package. It prints the full operation list — installs, upgrades, downgrades — without writing composer.lock, so you catch an unwanted major downgrade before it lands. If security advisories are driving the upgrade, pair this with the workflow in my post on how to read and fix a composer audit report, because the right answer to a CVE is the minimum version bump that clears it, not a blind composer update.

The error message already names the two constraints that disagree. Resolving a conflict is reading, not guessing — start at the Problem block, not the top of the output.Md Raihan Hasan

Why does it say my PHP version or extension is wrong?

Not every conflict is between two libraries. Some are against the platform itself — the PHP version, or an extension like ext-gd or ext-intl. These show up as requirements like php >=8.3 or ext-redis. Composer evaluates platform requirements against the PHP binary running Composer, which may not be the one running your app. Confirm both, then decide deliberately whether to override.

platform requirements
# What does Composer think your platform provides?
composer check-platform-reqs

# Pin the target PHP version so Composer resolves for production, not your laptop
# composer.json:  "config": { "platform": { "php": "8.3.0" } }

# Deliberately ignore ONE requirement (ext-redis is present in prod but not locally)
composer update --ignore-platform-req=ext-redis

# Blanket override - use only when you know the target environment differs
composer install --ignore-platform-reqs

Reach for --ignore-platform-req=ext-redis (the single, scoped form) over the blanket --ignore-platform-reqs. Ignoring one missing extension you know exists in production is reasonable; ignoring all of them silently installs packages your runtime can't actually load, and you find out at the first request instead of at composer install. Better still, set config.platform.php so resolution targets the deploy environment regardless of which PHP you run locally — the same discipline that keeps a Docker local development setup honest about parity with production.

What about minimum-stability, diagnose, and deleting the lock file?

If a required version only exists as a -beta or -dev release, Composer rejects it under the default stable setting. Don't drop minimum-stability to dev globally — that opens every dependency to unstable versions. Set it to the lowest stability you actually need and keep prefer-stable on so stable releases still win wherever they exist.

composer.json
{
    "minimum-stability": "beta",
    "prefer-stable": true,
    "require": {
        "acme/legacy-pkg": "^1.3"
    }
}

When the failure smells environmental — a hung download, a TLS error, the wrong PHP binary, a missing GitHub token hitting the API rate limit — run composer diagnose. It checks connectivity, your config, disk, and OAuth tokens, and it surfaces the boring infrastructure problems that masquerade as dependency conflicts. Flag details live in the official Composer CLI documentation.

And the move everyone reaches for in a panic: deleting composer.lock. It is almost never the fix. The lock file is your record of exactly what's deployed; removing it tells Composer to re-resolve the entire graph from scratch, which can quietly pull in newer versions across dozens of packages and turn one conflict into an integration test you didn't ask for. The conflict is still there — you've just hidden which package caused it.

Dependency hell feels random only until you treat the error as data. Read the Problem block for the two constraints that disagree, run composer why-not to confirm the blocker by name, then bump, loosen, or move exactly one thing and verify with --dry-run. Keep your constraints as wide as correctness allows, pin your platform to production, and leave composer.lock committed. Do that and "could not be resolved" stops being a wall and becomes a checklist — which matters most right before a major framework jump like a Laravel 11 to 12 upgrade, where every transitive constraint moves at once.