psecio/versionscan
VersionScan checks your PHP version and installed extensions against a database of known security vulnerabilities, flagging outdated or unsafe releases so you can confirm your runtime meets current security recommendations.
Install the scanner as a local dev dependency so it versions with the project and runs in CI:
composer require --dev psecio/versionscan
The binary lands at vendor/bin/versionscan. The first thing to run is a scan against a source directory, targeting the PHP version(s) you actually deploy to:
vendor/bin/versionscan scan src/ --php-version=7.4
The output is a report of functions, features, and language constructs found in your code that are not available (or behave differently) on the target PHP version, flagged with the version they were introduced or removed in. Treat the first run as a survey: point it at src/, not the whole repo, and not vendor/, because third-party code will dominate the report and drown out anything actionable in your own code.
The first genuinely useful use case is an upgrade audit. Before bumping composer.json from ^7.4 to ^8.1, scan with --php-version=8.1 to find removed functions (create_function, each, money_format), deprecated features, and signature changes that would break at runtime. That gives you a work list in one pass instead of discovering breakages through integration test failures.
Check vendor/bin/versionscan scan --help first — the exact flags for output format, PHP version targeting, and exclusions are what you'll build your workflow around, and they're the part most worth pinning down before writing any CI config.
Target the versions you support, and scan each. The tool is version-aware, so a library supporting 7.4 || 8.0 || 8.1 needs a scan per target rather than one against latest. Run all of them and treat any finding as breaking for that version's contract.
Gate merges in CI. Wire a composer script so the invocation is consistent locally and in the pipeline:
{
"scripts": {
"scan": "versionscan scan src --php-version=8.1"
}
}
Then fail the build on findings. Use a structured format (JSON/XML) rather than parsing the CLI output with grep — CLI output is for humans and will change between releases. Pipe the JSON into your CI's reporting step and let a small script decide whether the exit code or the finding count should fail the job.
Pair it with static analysis, don't replace it. Versionscan answers "is this symbol available on this version?" PHPStan/Psalm answer "is this type-correct?". Run both, because versionscan will happily report a function as fine on 8.1 while PHPStan tells you the argument types are wrong. The two overlap on method-signature changes but neither covers the other's ground.
Use it as a pre-upgrade gate, repeatedly. The natural workflow is: pin the current version, confirm a clean baseline, then flip to the target version and diff the findings. New findings between the two runs are exactly your migration surface. Keep that baseline run in CI so a new 8.2-only function doesn't sneak in unnoticed.
Scope with exclusions. Point the scanner at src/, app/, tests/ as separate invocations if you need different severities per tree — test code using a removed function is usually acceptable; application code is not. Some codebases keep generated/legacy directories permanently on an old PHP version; exclude those explicitly rather than living with a perpetually red build that everyone learns to ignore.
Dockerize for reproducibility. The scanner's own data set is bundled with the package version, so pin it in composer.lock and it behaves identically everywhere. Running it inside the same container image as your app avoids any surprise about which PHP binary and extensions are present.
It's a heuristic, not a vulnerability scanner. The description says "possible vulnerabilities," and that's accurate — the tool flags availability and behavior findings. A hit does not mean you're exploitable; it means the code may not run, or may run differently, on that version. Budget triage time and expect false positives, especially around dynamic dispatch.
Dynamic calls are invisible to it. $fn($arg), call_user_func($name, ...), $object->$method(), and anything built from strings or configuration won't be resolved. If your codebase leans on variable functions, framework magic, or reflection, the report is a floor, not a ceiling. Grep for call_user_func, $$, and ->{ separately.
Aliases and extensions produce noise. Functions from optional extensions (mysqli, gd, intl, bcmath) may be flagged if the scanner's model doesn't know your runtime always has them loaded. Confirm the extension is a hard composer.json requirement (ext-*) and note it as a known-acceptable finding rather than chasing it.
Deprecated ≠ removed. Read the version annotation on each finding. A function deprecated in 7.0 and removed in 8.0 behaves fine on your 7.4 target and breaks on the 8.0 one. Grouping findings by "breaks on target" versus "warns for the future" is the difference between a useful report and a wall of red.
The data set trails PHP releases. The version/function mapping ships with the package, so brand-new PHP minor versions may be under-represented until the package is updated. composer update psecio/versionscan before trusting a scan against the very latest release, and keep it on a Dependabot/Renovate schedule rather than installing once.
Exit codes and output are for automation, not archaeology. Verify what the tool returns when it finds issues before wiring it into a pipeline — a scanner that exits 0 on findings will silently produce green builds. If the exit code isn't what you need, derive the gate from the JSON output's finding count instead, which is stable and explicit.
Large codebases need scoping. The scan is AST-based and reasonably fast, but scanning tens of thousands of files produces long reports and slow CI runs. Split by directory, cache the result keyed on the lockfile plus source hash, and run the full scan nightly rather than on every commit.
Extension point worth knowing: it's a PHP-Parser consumer. If you need a rule it doesn't ship with — say, flagging a specific framework helper removed between minor versions — the cleanest path is usually a small custom static-analysis check (PHPStan custom rule, or a standalone PHP-Parser script) run alongside versionscan, rather than forking the package's data set.
How can I help you explore Laravel packages today?