A few weeks ago we upgraded firebase/php-jwt from v6 to v7 in our app. It sounds like a routine dependency bump — update the version constraint in composer.json, run composer update, done. But there was a catch: one of our other packages, socialiteproviders/microsoft, had a hard constraint on the old version. This is the classic transitive dependency conflict, and if you haven't run into it before, you will.
Here's how we handled it and how you should too.
What's a transitive dependency conflict?
Your composer.json only lists the packages you directly depend on. But each of those packages has its own composer.json, listing their dependencies — and those packages have their own, and so on. This tree is your full dependency graph.
A transitive dependency conflict happens when you want to upgrade Package A, but Package B — which you also require — has a hard constraint that prevents it from working with the new version of A.
In our case:
- We wanted:
firebase/php-jwt: ^7.0 socialiteproviders/microsoft(v4.7.1) declared:"firebase/php-jwt": "^6.8"
These two constraints are mutually exclusive. Composer can't install a version of firebase/php-jwt that satisfies both ^7.0 and ^6.8 simultaneously, so it would refuse — with a dependency resolution error.
Step 1: Understand the dependency graph before touching anything
The first move is to inspect what the problematic package actually requires:
composer show socialiteproviders/microsoft
This outputs the requires section:
requires
php ^8.0
ext-json *
firebase/php-jwt ^6.8
socialiteproviders/manager ^4.4
There it is. Before writing a single line, you know exactly what needs to change.
Step 2: Validate with a dry run
Rather than guessing whether a combination of versions will resolve, ask Composer directly using --dry-run:
composer require --dry-run "firebase/php-jwt:^7.0" "socialiteproviders/microsoft:^4.9"
Composer will either:
- Print a clean resolution plan with the exact versions it would install, or
- Print an error telling you exactly which package is blocking and why
In our case it resolved cleanly:
Lock file operations: 0 installs, 3 updates, 0 removals
- Upgrading firebase/php-jwt (6.x-dev => v7.0.5)
- Upgrading socialiteproviders/microsoft (4.7.1 => 4.9.1)
Nothing was written to disk. We just confirmed the upgrade path works.
Step 3: Update both packages in a single command
This is the part people get wrong. If you only update firebase/php-jwt:
# ❌ This will fail — socialiteproviders/microsoft still requires ^6.8
composer update firebase/php-jwt
Composer will refuse because the new firebase/php-jwt v7 conflicts with the installed socialiteproviders/microsoft v4.7.1. You must update both in one pass so Composer can solve the graph holistically:
# ✅ Composer solves both constraints together
composer update firebase/php-jwt socialiteproviders/microsoft
Output:
- Upgrading firebase/php-jwt (6.x-dev => v7.0.5): Extracting archive
- Upgrading socialiteproviders/microsoft (4.7.1 => 4.9.1): Extracting archive
No security vulnerability advisories found.
Step 4: Bump the constraint in composer.json
Don't forget to update your version constraints in composer.json to reflect the new minimums:
"firebase/php-jwt": "^7.0",
"socialiteproviders/microsoft": "^4.9",
Why ^4.9 instead of leaving it at ^4.1? Because ^4.9 documents intent: "we need at least 4.9, because that's the version that supports firebase/php-jwt v7." If you leave it at ^4.1, a future composer update could theoretically resolve back to 4.7.x in some edge case and silently reintroduce the conflict.
Step 5: Check your own code for API breakage
A major version bump means potential breaking changes. Grep for every place you use the package:
grep -r "Firebase\\JWT" app/ --include="*.php" -l
For firebase/php-jwt specifically, JWT::encode() kept the same signature in v7 — the only notable change was a stricter PHP type hint on the $key parameter (OpenSSLAsymmetricKey|OpenSSLCertificate|string vs the old loosely-typed $key). Since we pass strings (shared secrets and RSA private keys), there was nothing to change in our ZendeskJwtTokenBuilder or DocuSignJwtTokenBuilder.
Step 6: Run the affected tests
XDEBUG_MODE=off php artisan test --compact \
tests/Unit/Domains/Tokens/JwtTokenBuilderTest.php \
tests/Feature/Authentication/MicrosoftLoginTest.php
Tests: 27 passed (106 assertions)
Duration: 10.44s
Green. Ship it.
The general playbook
Whenever you hit a transitive dependency conflict in Composer:
| Step | What to do |
|---|---|
| 1 | composer show <blocking-package> — find the conflicting constraint |
| 2 | composer require --dry-run "a:^X" "b:^Y" — validate resolution without touching files |
| 3 | composer update a b — update all conflicting packages in one pass |
| 4 | Tighten the version constraint in composer.json to document the new minimum |
| 5 | Grep for usages, read the changelog for breaking API changes |
| 6 | Run the affected tests |
The core insight: Composer's constraint solver needs to see the full picture. If you update one package at a time when there's a conflict, you're fighting the solver instead of working with it.
If this post was enjoyable or useful for you, please share it! If you have comments, questions, or feedback, you can email my personal email. To get new posts, subscribe use the RSS feed.