Every time I have prepared a Laravel project for a major version upgrade, the same problem appears. I run composer outdated and get a list of version numbers. I run composer audit and check for known CVEs. Then I spend the next hour or two opening Packagist pages and GitHub repositories for each package that looks suspicious, trying to answer questions the tools did not answer.
Is this package still actively maintained? When was the last release? Does it support the Laravel version I am moving to? Is the latest version blocked by my composer.json constraint? Is the GitHub repository archived? These are the questions that actually matter before an upgrade, and none of the standard Composer commands answer them together.
Laravel Package Doctor is a Composer package that does.
What Composer gives you vs what it means
When you run composer outdated on a project with a problematic package, you get something like this:
vendor/legacy-helper 1.2.0 2.0.0
That tells you a newer version exists. It does not tell you whether the upgrade is safe, whether your current constraint allows it, or whether the package has been abandoned by its author.
Laravel Package Doctor runs the same scan and produces:
vendor/legacy-helper
Current: 1.2.0
Latest: 2.0.0 (Major upgrade — may contain breaking changes)
Latest allowed: 1.2.3 (Latest version your constraint permits)
Score: 42 / 100
Status: Risky
Issues:
↳ [constraint_blocked] Latest version is blocked by your composer.json constraint.
↳ [major_upgrade_available] A major upgrade is available. Review changelog before updating.
Recommendation: Review changelog and update constraint if compatible.
For a genuinely critical package, it goes further:
vendor/abandoned-auth
Current: 0.9.1
Latest: 0.9.1
Score: 14 / 100
Status: Critical
Issues:
↳ [abandoned] Package is marked as abandoned on Packagist.
↳ [no_release_18_months] No release in over 18 months.
↳ [laravel_incompatible] Does not declare support for current Laravel version.
Recommendation: Replace this package before upgrading Laravel.
The raw version diff becomes a scored decision.
What the package detects
Each dependency gets a health score from 0 to 100. The score starts at 100 and weighted deductions apply per issue:
- Security advisory found: −30
- Package abandoned on Packagist: −30
- GitHub repository archived: −25
- Laravel version incompatibility: −20
- PHP version incompatibility: −20
- Constraint blocking latest major: −15
- No release in 18 months: −15
- Risky license (GPL/AGPL variants): −15
- Major upgrade available: −10
- No release in 12 months: −8
Scores map to four statuses: Healthy (90–100), Watch (70–89), Risky (40–69), Critical (0–39). The project as a whole gets an aggregate score.
The full report is a table: package name, current version, latest version, upgrade type (patch/minor/major), score, status, and one recommendation per package.
Installing it
composer require --dev satheez/laravel-package-doctor
Laravel auto-discovers the service provider. There is no manual registration step. Running php artisan package:doctor is the entire quick start.
If you want to tune thresholds, CI gates, scan scope, or silence known false positives:
php artisan vendor:publish --tag=package-doctor-config
The config exposes a minimum_project_score for CI, which packages to ignore, whether to include dev or transitive dependencies, and a GitHub token for projects large enough to hit rate limits.
The workflows I actually use it for
Before a Laravel upgrade — direct dependencies only, because those are the ones you control:
php artisan package:doctor --direct
This shows every direct dependency that has not declared support for the target Laravel version, every one that is abandoned, and every one where a major upgrade is available but blocked by your constraint. That list is the pre-upgrade work list.
Gating a production deployment — exit codes make this composable with any CI system:
php artisan package:doctor --no-dev --ci
Exit code 0 = all production packages are healthy enough to deploy. Exit code 2 = at least one critical package was found; the pipeline stops. No parsing required.
Auditing an inherited project — full scan, all dependencies:
php artisan package:doctor
Five minutes after cloning an unfamiliar project, you have a scored picture of every dependency. Abandoned packages, archived repos, constraint-blocked upgrades, license concerns — all surfaced in one pass instead of an afternoon of Packagist tab-switching.
Weekly health check — store the result as a CI artifact:
php artisan package:doctor --json --ci
JSON output is stable enough to pipe into dashboards or diff week over week. The --ci flag ensures the exit code is meaningful even when output is redirected.
Why not Dependabot or composer audit
These tools solve different parts of the problem, and they compose well.
composer audit and Dependabot alerts are hard security gates. Run them. They are fast, authoritative for known CVEs, and integrate directly with GitHub. Package Doctor does not replace them.
Dependabot and Renovate automate routine version bumps. Let them. Automated minor and patch updates are noise you do not want to manage manually.
Package Doctor is the layer that answers the questions Dependabot and composer audit do not — abandonment, compatibility with your target Laravel version, constraint-blocked majors, release recency, license classification. It is the decision layer that sits above the raw data these tools provide.
The README has a full comparison table if you want to see exactly which tool covers which concern.
What it deliberately does not do
Package Doctor never modifies your project. It does not run composer update. It does not rewrite composer.json. It does not install or remove anything.
It reads your lock file, runs read-only Composer commands, and calls Packagist and GitHub APIs. The output is a report. What you do with the report is your decision.
The package is on GitHub at satheez/laravel-package-doctor and available via Packagist. If you inherit Laravel projects regularly or are approaching a major version upgrade, it is the first thing I reach for before touching composer.json.