maglnet/composer-require-checker
ComposerRequireChecker is a CLI tool that analyzes your composer.json and source code to find unknown symbols and soft/transitive dependencies. It flags code relying on packages or PHP extensions you haven't explicitly required, helping prevent unexpected breakage from dependency updates.
ComposerRequireChecker is a CLI tool, not a library — you don't require it into a project. In a Laravel codebase the fastest path is the PHAR:
# Download once, keep it out of the repo (or gitignore it)
curl -L -o composer-require-checker.phar \
https://github.com/maglnet/ComposerRequireChecker/releases/latest/download/composer-require-checker.phar
chmod +x composer-require-checker.phar
# Baseline run against your app
composer install
php composer-require-checker.phar check composer.json
Alternatives: phive install composer-require-checker if you use Phive for tooling, or composer global require maglnet/composer-require-checker to get a composer-require-checker binary on your PATH. Do not add it to require-dev — it drags in symfony/console, nikic/php-parser, etc., and pollutes the very dependency graph it's supposed to audit.
Where to look first:
composer installed Laravel app. You'll likely get a mix of core-symbol whitelist complaints and genuine soft dependencies — this is your baseline.data/config.dist.json (linked from the README) into your project as composer-require-checker.json and edit it. This is the default config filename the tool picks up automatically.guzzlehttp/guzzle, nesbot/carbon, monolog/monolog, symfony/*, league/flysystem, ramsey/uuid, etc. that you use but didn't declare — either add to require/require-dev or, if transitive-by-design, whitelist the specific symbol.First realistic use case: a Laravel app using Http:: (Guzzle under the hood), Carbon, and Str/Arr helpers. The tool flags every Guzzle class you touched directly, plus any PHP extension functions (e.g. intl, sodium, gd, bcmath, redis) you rely on that aren't in composer.json's require ext-* section.
Keep the PHAR download on every run and cache it keyed by a pinned version, then feed the config file:
# .github/workflows/require-checker.yml
- name: Install ComposerRequireChecker
run: |
curl -fsSL -o composer-require-checker.phar \
https://github.com/maglnet/ComposerRequireChecker/releases/download/4.9.0/composer-require-checker.phar
- name: Check hard dependencies
run: XDEBUG_MODE=off php composer-require-checker.phar check --config-file=composer-require-checker.json composer.json
Pin a release tag rather than latest — the default core-symbol whitelist shifts between minors (see 4.6.0 adding hash, 4.7.0 touching /etc/shadow detection, 4.8.0 dropping PHP 8.1) and a floating tag can silently fail your build.
The tool reads exactly one config file. That means your project config must be a superset of the dist whitelist. The maintainable pattern:
data/config.dist.json at the version you pin.cp vendor/.../config.dist.json or a curl of the tagged file) whenever you bump the tool.scan-files, extra symbol-whitelist entries) into that copy.composer-require-checker.json.This avoids the "why is true undefined?" trap that happens when someone hand-writes a minimal config.
Laravel has plenty of PHP outside PSR-4 autoloading — bin/artisan style scripts, custom artisan-adjacent entrypoints, deployment scripts. Use scan-files with globs:
{
"scan-files": ["bin/console", "bin/*.php", "database/seeders/*.php", "routes/*.php"]
}
Where practical, prefer adding the path to composer.json's autoload/autoload-dev (e.g. classmap or files) so the tool's default autoload discovery picks it up and you don't have to maintain a list of globs. Reserve scan-files for files that genuinely shouldn't be autoloaded.
Treat additions to symbol-whitelist like a code review event, not a rubber stamp. A good convention: annotate each entry with a // symbol — reason — owner comment (JSON doesn't support comments, so keep a parallel docs/require-checker-whitelist.md if your team values the paper trail). Distinguish two kinds:
true, false, null, Traversable, DateTimeInterface, etc. These belong in the inherited dist block; never add anything here by hand unless the tool version didn't ship it.Psr\Log\LoggerInterface when you only require psr/log indirectly, or a class from a package you deliberately don't want to take a hard dep on. Each one should be a conscious decision.PHP extension functions surface as unknown symbols when ext-* isn't declared. The tool has a built-in map of core extension → symbols. Two approaches:
"ext-intl": "*" (or the real constraint) to composer.json's require block. This is a real correctness fix and makes Composer enforce it.function_exists().Hook it into a composer script so it runs alongside Pint/Stan:
{
"scripts": {
"check": [
"@php vendor/bin/pint --test",
"@php vendor/bin/phpstan analyse",
"@php composer-require-checker.phar check --config-file=composer-require-checker.json composer.json"
]
}
}
Guard it behind function_exists('Composer\\Config::disableProcessTimeout') style checks or simply document that contributors need the PHAR installed; otherwise every composer check for a new contributor becomes a scavenger hunt.
The README calls this out for a reason. Xdebug's max-nesting-level and slowdown cause false "exceeded max nesting" errors or timeouts on Laravel-sized codebases. Always run with:
XDEBUG_MODE=off php composer-require-checker.phar check composer.json
Bake this into every CI script and composer script entry. On a large app it's the difference between ~5s and a timeout.
The tool reads Composer's vendor-dir from composer.json and assumes standard layout — it does not boot Composer. Anything installed by a composer-plugin to a non-vendor/ path will confuse it. The documented workaround:
composer install --no-plugins
php composer-require-checker.phar check composer.json
composer install
If you rely on installer plugins in your app, make this a dedicated just check-deps / make check-deps recipe rather than something people have to remember by hand.
If you create composer-require-checker.json with only "scan-files" and nothing else, the tool won't apply its default whitelist. Every core symbol (true, null, Traversable, every internal function from every bundled extension) becomes an error. Symptom: your CI suddenly reports hundreds of "undefined symbol true" style failures. Fix: copy the whole dist config, edit inside it. This trips up virtually every new user once.
The tool silently picks up composer-require-checker.json from the current working directory if --config-file isn't passed. This is convenient but hides mistakes: a stray file in a subdirectory won't be found, and CI running from a different directory won't see it. Be explicit in CI: --config-file=$(pwd)/composer-require-checker.json.
composer.json require-style declarationThe tool inspects require and require-dev. If you declare a hard dep only in suggest (common in packages but not apps) it won't count. If your app relies on a class that is only suggest-ed, you'll get a false positive and need a whitelist entry.
New extensions and newer core functions occasionally aren't yet in the tool's symbol map. Symptom: an internal function flagged as unknown even though it's obviously core. Check the pinned version's data/extensions.json (or equivalent) for coverage, and either bump the tool or whitelist the function with a comment. The 4.6.0 release notes adding hash are a good example of this drift.
4.8.0 dropped PHP 8.1 and added Symfony 7 support; 4.7.0 enabled PHP 8.3. Pin the PHAR version and make sure it's runnable on the oldest PHP you support, or run it only on your newest PHP in CI. Running a too-new PHAR on an old PHP produces fatal errors rather than diagnostics.
4.9.0 added attribute support. If you're on an older 4.x and use PHP attributes (common in Laravel 10+/11 with route/attribute-style configuration), you may see symbols used only in attributes slip through undetected — the inverse of a false positive. Upgrade to ≥4.9.0 before trusting the output on attribute-heavy code.
When the check fails, pipe the output to a file and inspect the "soft dependency" list separately from the "core extension" list. Most failures fall into three buckets: (1) forgot ext-* in require, (2) using a transitive package without declaring it, (3) config whitelist not being applied. Addressing them in that order — extension declarations, then hard-deps, then whitelist — resolves the overwhelming majority of red builds in one pass.
How can I help you explore Laravel packages today?