sebastian/lines-of-code
A lightweight PHP library from Sebastian Bergmann for counting lines of code in PHP source files, reporting logical and physical line counts. Used by PHPUnit's code coverage tooling and handy for building metrics, reports, and static analysis tools.
Architecture fit. sebastian/lines-of-code is a stateless, framework-agnostic, single-purpose library: you hand Counter::count() a file path and get back an immutable value object exposing linesOfCode, commentLinesOfCode, and nonCommentLinesOfCode. There are no service-provider bindings, no configuration surface, no I/O beyond reading the file, and no persistence. Architecturally that puts it in the build-time analytics tier of a Laravel system — an Artisan command, a scheduled reporting job, a CI step, a PR-comment bot — and explicitly not in the request lifecycle. For a TPM the consequence is favorable: zero production latency, zero runtime blast radius, no security review of an inbound attack surface. The decision is therefore almost entirely a process decision (what the number is for, who owns the threshold, what decision it feeds), not a technical one.
Integration feasibility. High. Installation is a single Composer line, and it should be --dev unless a reporting job ships in the deployed artifact. The PoC is genuinely half a day: one recursive directory walk, an exclusion list, an aggregation, and an output format. Two caveats materially shape the estimate:
phpunit/php-code-coverage, so it is very likely already in your composer.lock on any Laravel project running PHPUnit. Run composer why sebastian/lines-of-code before scoping — if it is already present, the marginal dependency and procurement risk is effectively nil and the go/no-go conversation changes shape.vendor/ and .gitignore exclusion, aggregation by module, and the tests for all of that are your code. Budget one to two days of engineering including validation, not one hour.Technical risk. Four risks, ordered by probability of derailing the initiative:
| # | Risk | Likelihood | Impact | Mitigation |
|---|---|---|---|---|
| 1 | PHP version floor. 5.x dropped PHP 8.3 support (requires 8.4+); 4.x dropped 8.2; 3.x targets 8.2. Many Laravel LTS-track apps cannot take the current major without a runtime upgrade. | High | Blocks adoption of latest line | Pin deliberately: 5.x on 8.4+, 4.x on 8.3, 3.x on 8.2. Or run the counter in an isolated CI container pinned to 8.4 and keep app runtimes untouched. |
| 2 | Counting correctness. 4.0.1 and 5.0.1 fix the same defect — multi-comment lines double-counted, producing negative non-comment LOC. 5.0.2 followed with a guard forcing $linesOfCode positive. A metric that can return impossible values is a governance hazard if it gates merges. |
Medium | Credibility loss on first bad dashboard | Golden-file validation against a hand-counted sample and against cloc before any enforcement. Assert non-negativity in your own wrapper. |
| 3 | Parser coupling. It depends on nikic/php-parser, shared with PHPStan, PHP-CS-Fixer, and Rector. (Note: internal docs describe it alternately as a tokenizer and a parser — in practice it is parser/tokenizer-based, which is exactly why heredocs and block comments are handled correctly, and exactly why it couples you to that stack.) |
Medium | A broken composer update, not a broken metric — noisy and diagnosable |
Align the nikic/php-parser constraint with the existing static-analysis stack; let Composer resolve before locking a version. |
| 4 | Scope blindness. PHP source only. Blade templates, Livewire/Inertia, JS/Vue/React, migrations, and config are invisible. On a full-stack Laravel team this systematically under-reports delivered work. | Certain | First dashboard gets challenged and loses authority | State the boundary in the dashboard's own subtitle: "PHP application code only." Pair with a polyglot counter (tokei/cloc) if the audience expects whole-repo totals. |
Key questions for the team.
A governance caveat a TPM must raise explicitly. LOC is a proxy metric and degrades badly under pressure. The moment it becomes a per-developer productivity score, engineers optimize for it — splitting PRs, stripping comments (which this library conveniently counts separately, inviting a "comment density" score that is worse than useless), deleting scaffolding. The signal inverts and the dashboard becomes actively harmful. Used as a team-level trend against a rolling baseline, it is a cheap, defensible input to refactoring and capacity conversations. Get that sentence into the rollout doc before the first dashboard ships, co-signed by the engineering manager who will be asked about it in a performance calibration.
Stack fit. Strong on three Laravel-native surfaces, poor on one:
| Surface | Fit | Rationale |
|---|---|---|
Artisan command (metrics:loc) |
Strong | Idiomatic, discoverable, zero infrastructure, no new deployment artifact. Recommended first deliverable. |
| PHPUnit assertion / test guard | Strong | Good for a narrow, high-value check: "app/Modules/Billing must not exceed N logical LOC without review." Keep it markTestSkipped-adjacent (warn-only) initially. |
| Scheduled job + persisted history | Moderate | The only path to a trend line. Requires a table, a migration, and an owner. Justify it against "we already have a BI tool that could ingest a CSV." |
| HTTP endpoint / request lifecycle | Avoid | Reads the filesystem synchronously and has no business in a user-facing request. Any dashboard should read persisted results, not count on demand. |
Migration path. There is nothing to migrate from if this replaces an ad-hoc wc -l script or a spreadsheet — and nothing to migrate to if you already have SonarQube, CodeClimate, or PHPMetrics producing LOC data, in which case adding this creates a second source of truth and a reconciliation argument you do not want in a steering meeting. The realistic migration is: retire informal size anecdotes, not retire a tool. Sequence it as additive first, and only decommission the shell script once the new numbers are validated and someone actually reads them weekly.
Compatibility. Three axes to check before committing:
nikic/php-parser — confirm the major version this package pulls matches what PHPStan / PHP-CS-Fixer / Rector already require in the lockfile.phpunit/php-code-coverage, check whether an explicit direct dependency is even necessary or whether you are just formalizing a transitive one. Formalizing it (adding it explicitly with a pinned constraint) is generally the right move — it prevents a PHPUnit upgrade from silently removing a capability you now depend on.Sequencing. Four steps, each independently shippable and independently abortable:
cloc. If they disagree and you cannot explain why, stop here.vendor/, node_modules/, storage/, generated code). Emit a table and a --json flag. No persistence.Do not skip to step 4. The failure mode of this package is not technical; it is an unvalidated number acquiring policy authority.
Maintenance. Very low — and this is the package's strongest operational argument. It is a roughly hundred-line component maintained by the PHPUnit maintainer, versioned with clear semantic major releases, CI- and coverage-gated. The maintenance cost sits almost entirely in your wrapper (traversal rules, exclusion lists, aggregation, storage schema), not in the dependency. Expect to touch your wrapper when repositories are restructured and essentially never to touch the library. Budget a few hours a year for dependency bumps.
Support. No vendor, no SLA, no paid tier — BSD-3-Clause, best-effort open source. For a build-time metrics tool that is acceptable; for anything customer-facing it would not be. Internalize this: if the package is abandoned or a future major takes a direction you cannot follow, the exit is trivial (the output is three integers; swap in tokei or a tokenizer you write in an afternoon). Exit cost is near zero, which is exactly what justifies the low-risk "optional infrastructure" framing. The real support burden is social: someone must own "what does this number mean" and answer the question in every review where it appears.
Scaling. Compute scales linearly and trivially — counting a 50K-file tree is a CI-minute-scale job, not an infrastructure concern. What does not scale is the narrative. Two specific scaling limits:
Failure modes. Five, with detection:
| Failure | Symptom | Detection / response |
|---|---|---|
| Negative or zero non-comment LOC on comment-heavy files | Impossible values in output | Assert >= 0 and >= physical-count invariants in your wrapper; fail the job loudly rather than writing bad rows. Precedent exists in 4.0.1/5.0.1/5.0.2. |
| Silent misconfiguration of exclusions | Total jumps when vendor/ ships in a release tarball, or drops when a path is over-excluded |
Assert total is within ±X% of prior run; alert on step changes, not just on thresholds. |
| Parser version conflict | composer update fails; CI red for an unrelated reason |
Standard dependency hygiene; keep the constraint aligned with the static-analysis stack. |
| Metric gaming | Comment density falls, PR size fragments, scaffolding deleted | The most dangerous failure because it looks like improvement. Never publish per-person. Review the trend, not the leaderboard. |
| Dashboard abandonment | Nobody opens it after week three | Pre-commit to a named consumer and a monthly review cadence, or don't build step 3 at all. An unread dashboard is pure carrying cost. |
Ramp-up. Minimal for engineers — one constructor call, one method, one value object; a competent Laravel developer is productive in under an hour with the README alone. The real ramp-up cost is organizational, not technical, and should be planned as such:
Bottom line for a TPM: treat this as optional internal infrastructure with near-zero exit cost, not a roadmap item. It is worth adopting only if you specifically need PHP-aware, in-process LOC — including logical/statement counts — feeding a trend you will actually review monthly. If the requirement is merely "a number in a dashboard," cloc in CI gets you 80% of the value for near-zero engineering. The adoption decision should be made on the strength of the decision the trend feeds, not on the library's merits — those are settled and unremarkable, which is precisely the compliment this dependency deserves.
How can I help you explore Laravel packages today?