Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Lines Of Code Laravel Package

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.

View on GitHub
Deep Wiki
Context7

Getting Started

1. Confirm you already have it. This package is a transitive dependency of phpunit/php-code-coverage, so most Laravel apps already ship it in composer.lock:

composer why sebastian/lines-of-code

If the answer is phpunit/php-code-coverage, you can start experimenting immediately. If you want it as a declared dependency (recommended the moment you call it from app code), add it explicitly:

composer require --dev sebastian/lines-of-code

2. Pin against your PHP runtime, not the latest tag. This is the first thing that will bite you, and it has nothing to do with Laravel:

Line Minimum PHP
5.x 8.4
4.x 8.3
3.x 8.2

Laravel 11/12 commonly run on 8.2/8.3. Constrain deliberately — "sebastian/lines-of-code": "^4.0" — rather than letting ^5.0 in and discovering it during a composer update on someone else's branch.

3. Look at exactly two classes. There is no config, no facade, no service provider, no docs site. The entire public surface is:

  • SebastianBergmann\LinesOfCode\Counter — one method, countInFile(string $filename): LinesOfCode
  • SebastianBergmann\LinesOfCode\LinesOfCode — four accessors: linesOfCode(), commentLinesOfCode(), nonCommentLinesOfCode(), logicalLinesOfCode()

Read those two files in vendor/ before reading anything else. That is the whole API.

4. First use case — a tinker one-liner, so you can see real numbers against your own code:

$counter = new SebastianBergmann\LinesOfCode\Counter;
$result  = $counter->countInFile(app_path('Models/User.php'));

$result->linesOfCode();           // physical lines
$result->commentLinesOfCode();    // comment lines
$result->nonCommentLinesOfCode(); // total minus comment lines
$result->logicalLinesOfCode();    // statements, not lines

Sanity-check the arithmetic before going further — a correct result satisfies nonCommentLinesOfCode() + commentLinesOfCode() === linesOfCode(). If that ever fails you are on a version predating the 4.0.1/5.0.1 fix.

5. Then build the Artisan command. That is the idiomatic Laravel first deliverable and where 90% of teams stop:

class LocCountCommand extends Command
{
    protected $signature = 'loc:count {path=app} {--json}';

    public function handle(): int
    {
        $counter = new Counter;
        $total   = ['lines' => 0, 'comment' => 0, 'non_comment' => 0, 'logical' => 0];

        foreach (File::allFiles(base_path($this->argument('path'))) as $file) {
            if ($file->getExtension() !== 'php') {
                continue;
            }

            $loc = $counter->countInFile($file->getPathname());

            $total['lines']       += $loc->linesOfCode();
            $total['comment']     += $loc->commentLinesOfCode();
            $total['non_comment'] += $loc->nonCommentLinesOfCode();
            $total['logical']     += $loc->logicalLinesOfCode();
        }

        $this->option('json')
            ? $this->line(json_encode($total))
            : $this->table(array_keys($total), [$total]);

        return self::SUCCESS;
    }
}

Note what you just wrote: all the traversal, all the exclusion logic, all the aggregation. That is not a missing feature — it is the package's entire scope.

Implementation Patterns

Wrap it once, use it everywhere. Put the walker behind your own class rather than calling Counter from three places. Counter is final and construction-free, so there is nothing to mock; your wrapper is the seam for tests.

namespace App\Support\Metrics;

final class LocScanner
{
    public function __construct(private readonly Counter $counter = new Counter) {}

    /** @return array<string, array{lines:int,comment:int,non_comment:int,logical:int}> */
    public function scanModules(string $root = 'app/Modules'): array
    {
        $out = [];

        foreach (File::directories(base_path($root)) as $module) {
            $out[basename($module)] = $this->scan($module);
        }

        return $out;
    }
}

Use git ls-files as your file list. It solves .gitignore, vendor/, node_modules/, and generated artifacts in one line, and it is faster and more correct than hand-rolled RecursiveDirectoryIterator exclusion lists:

$files = explode("\n", trim(Shell::exec('git ls-files "*.php"')));

The four patterns worth building, in order of value:

  1. php artisan loc:count --json — the primitive. Discoverable, zero infra, zero production footprint.

  2. Nightly snapshot job + table. This is the highest-value pattern and the one most teams skip. Without persistence there is no trend, and a trend is the only version of this metric that survives a steering meeting. Keep it boring:

    Schema::create('loc_snapshots', function (Blueprint $t) {
        $t->id();
        $t->string('scope');            // 'app', 'app/Modules/Billing', ...
        $t->unsignedInteger('lines');
        $t->unsignedInteger('comment_lines');
        $t->unsignedInteger('non_comment_lines');
        $t->unsignedInteger('logical_lines');
        $t->string('commit_sha')->nullable();
        $t->timestamp('captured_at');
    });
    

    Schedule it via Schedule::command('loc:snapshot')->dailyAt('02:00')->onOneServer().

  3. Per-file cache keyed by content hash. A full scan of a mid-size Laravel app is roughly a second; a dashboard endpoint called by fifty people is not. hash_file('xxh128', $path) as the cache key makes repeat scans near-free and survives branch switches correctly.

  4. Warn-only PR comment. In CI, count the merge base and the head, post the delta. Never make it blocking in the first quarter — see the governance note below.

Pair with PHPUnit but keep it out of the default suite. A budget test is a nice soft guardrail:

#[Group('metrics')]
public function test_billing_module_has_not_grown_beyond_budget(): void
{
    $loc = (new LocScanner)->scan(app_path('Modules/Billing'));

    $this->assertLessThan(25_000, $loc['non_comment'], 'Billing module exceeded size budget.');
}

Run it in a separate CI job (--group metrics) so a deliberate refactor spike never blocks a hotfix.

Report by module, not by file. Per-file numbers are noise. Directory-level aggregation — "Billing grew 40% this quarter while shipping three features" — is the sentence that actually changes a decision, and it is a groupBy on the first path segment away.

Gotchas and Tips

nonCommentLinesOfCode is not "code lines". The arithmetic is total − comment, which means blank lines land in the non-comment bucket. If you quote a "code density" figure, verify it against a hand-counted sample first — a blank-line-heavy file will look denser than it is. Use logicalLinesOfCode() when you want a statement count that blanks and formatting cannot inflate.

Validate against cloc or tokei on day one. Both 4.0.1 and 5.0.1 fixed the same defect: lines containing multiple comments were double-counted, producing a negative nonCommentLinesOfCode. A metric that can silently return an impossible number is a governance hazard the moment it gates anything. 5.0.2 adds a positivity guarantee — pin ^5.0.2 or ^4.0.1 at minimum, and diff your first full run against cloc --include-lang=PHP before publishing a single chart.

Catch per file, not per scan. countInFile throws NoSuchFileException and RuntimeException on unreadable or vanished files. One stale path — a file deleted between git ls-files and the read, a broken symlink — should not blank the whole report:

try {
    $loc = $this->counter->countInFile($path);
} catch (NoSuchFileException | RuntimeException $e) {
    report($e);
    continue;
}

Skip symlinked directories during traversal. A symlink back up the tree gives you an infinite walk and a very confusing memory profile.

It counts PHP only. Blade templates, Livewire components, Vue/React, migrations, config, and _ide_helper.php are invisible or actively misleading. For a full-stack Laravel team this systematically under-reports work — say so before the first dashboard ships, or the first reviewer who notices will discredit the whole thing.

Check the real dependency graph before assuming a conflict. Run composer show sebastian/lines-of-code and read the requires block. If it pins a parser version shared with PHPStan, PHP-CS-Fixer, or Rector, align them deliberately — a mismatch surfaces as a broken composer update, which is noisy but diagnosable, not a silent wrong number.

Never call it in the request lifecycle. A metrics page that walks app/ on every page load will be the slowest route in your app on a large repo. Read from loc_snapshots or the cache; write on a schedule.

There are no extension points. No config file, no events, no drivers, no interfaces to implement. Your wrapper class is the extension point — which is fine, but it means any behaviour you want (exclusions, grouping, caching, thresholds) is code you own and test.

Governance, stated once and loudly. Team-level trend against a rolling baseline: useful, cheap, defensible. Per-developer LOC or a blocking merge gate: engineers will split PRs, strip comments, and delete scaffolding, and the signal inverts. Put that in the rollout doc before the first dashboard link is shared, and keep any CI check warn-only for at least a quarter.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
socialdept/tenant-domains
tautvydasr/blade-class-names
socialdept/color-scales
maherelgamil/controller
coleus/widgets
coleus/table
coleus/users
coleus/music
coleus/health
coleus/calendar
coleus/notes
coleus/support
coleus/settings
coleus/apps
seampact/laravel
emmyconceptng/laravel-ng-postcode
jjsquady/round-robin
skyraptor/chatter
edlugz/sasapay
abbasudo/artisan-route