cmgmyr/phploc
Measure the size and structure of a PHP project with phploc: lines of code, cyclomatic complexity, dependencies, and class counts, right from the command line.
cmgmyr/phploc is not a Laravel package — it is the maintained fork of sebastianbergmann/phploc, a standalone CLI binary. There is no service provider, no config file, and no Artisan command. Treat it as a shell tool you invoke from Composer scripts, a Makefile, or a small custom Artisan wrapper.
1. Install (project-local, not global)
composer require --dev cmgmyr/phploc
vendor/bin/phploc --version
Prefer local install over the README's composer global install: it pins the version per project (so your trend numbers stay comparable) and avoids the ~/.composer/vendor/bin PATH problem. If you do install globally, remember the binary lands in ~/.config/composer/vendor/bin on modern Composer, not ~/.composer/vendor/bin.
2. Point it at your code, excluding the noise
vendor/bin/phploc app \
--exclude vendor \
--exclude storage \
--exclude bootstrap/cache \
--exclude node_modules
There is no JSON output. For anything programmatic use --log-csv / --log-xml (confirm the exact flag names against vendor/bin/phploc --help on your pinned version, since option sets shifted between majors).
3. Read the four numbers that actually matter
Skip the wall of output on first run and look at:
First use case: baseline your worst legacy module before you touch it. Run phploc app/Legacy --log-csv=before.csv, do the refactor, run it again into after.csv, then diff the LLOC and max-complexity rows. This is the single highest-value use: proving a refactor actually reduced size/complexity rather than just moved code around.
Composer script for repeatable invocations
{
"scripts": {
"metrics": "phploc app --exclude vendor --exclude storage --exclude node_modules tests",
"metrics:csv": "phploc app --exclude vendor --exclude storage --log-csv=build/phploc.csv"
}
}
Then composer metrics. Keeps flags in one place so nobody hand-types a subtly different command and produces incomparable numbers.
Per-directory slicing instead of whole-app runs
Whole-repo runs on a large Laravel app give you an average that hides everything. Run per bounded context and compare:
for dir in app/Models app/Http/Controllers app/Services app/Console; do
echo "== $dir"; vendor/bin/phploc "$dir" --exclude vendor | head -20
done
The directory with the highest maximum method complexity — not the highest LLOC — is where you start.
Wrapping it in an Artisan command
Because the output is text/CSV, the clean integration is a thin command that shells out and persists the numbers for trending:
// app/Console/Commands/CollectMetrics.php
$csv = storage_path('app/metrics/phploc-'.now()->format('Ymd').'.csv');
Process::run([
base_path('vendor/bin/phploc'), 'app',
'--exclude', 'vendor',
'--exclude', 'storage',
"--log-csv={$csv}",
])->throw();
// parse with fgetcsv() and upsert into a metrics table,
// or compare against the previous row and fail the build on regressions
Schedule it weekly, store rows keyed by commit/tag, and you get a size-and-complexity trendline for free. Note that phploc itself never exits non-zero on bad numbers — the threshold logic is yours.
CI: artifact, not gate
- run: composer metrics:csv
- uses: actions/upload-artifact@v4
with:
name: phploc
path: build/phploc.csv
Upload the CSV per build. Gating directly on phploc output is brittle because the metric definitions (especially LLOC) have changed between majors; gate on your own parsed threshold with a pinned phploc version instead.
Pair it with a rules engine, don't replace one
phploc measures; it does not enforce. Use phploc to find hotspots ("which classes are 400+ lines, which methods exceed complexity 20"), then use PHPStan/PHP_CodeSniffer/PHPMD to enforce the resulting rules in CI. Reaching for phploc as a quality gate directly is the most common misfire with this tool.
--count-tests is a deliberate choice
By default test code should be excluded along with vendor. Pass --count-tests only when you genuinely want test LOC folded into the totals — for a "how much code do we maintain" figure it will distort the ratio.
app/, routes/, database/" is wrong. It walks whatever path you give it and nothing else.vendor/ is not reliably excluded. Always pass --exclude vendor explicitly. A run that silently swept vendor/ will produce numbers an order of magnitude too large, and it's an easy mistake to miss because the output still looks plausible.storage, bootstrap/cache, node_modules, and Blade. phploc is tokenizer-based; .blade.php files contain almost no analyzable PHP tokens and just add noise. Restrict to --suffix .php and point at real PHP directories.SebastianBergmann\PHPLOC became Cmgmyr\PHPLOC in 8.0.0. If you had any code referencing the old classes directly (rather than the binary), it breaks. Same for anyone copy-pasting old blog posts about programmatic use.--names and --names-exclude were removed in 7.0.0 — use --suffix and --exclude. Stack Overflow answers referencing the removed flags are still the top hits."cmgmyr/phploc": "8.0.8", not ^8.0. LLOC and complexity counting have shifted across releases, so an unpinned range silently invalidates your historical trend comparison. This is the single most important operational rule if you're tracking numbers over time.composer.json says ^7.4 || ^8.0, but the transitive deps (php-file-iterator, cli-parser) have moved forward. On PHP 8.3/8.4 you generally need a recent 8.0.x release; on older PHP you may need to stay back. Check vendor/bin/phploc --version after install rather than trusting the constraint.--threshold, no --fail-on. Any gate must parse the output yourself — which is why --log-csv matters more than the human-readable table.fgetcsv() plus array_combine() on the header row is enough; the CSV is a flat metric/value grid rather than one-row-per-class, so don't expect per-class granularity from it.composer.lock, and keeping sebastianbergmann/phploc as a documented fallback if the fork stalls. Nothing in your workflow should depend on the fork's namespace or internals — only on the binary and its CSV.How can I help you explore Laravel packages today?