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

Pyrameter Laravel Package

boundwize/pyrameter

View on GitHub
Deep Wiki
Context7

Getting Started

Install as a dev dependency.

composer require --dev boundwize/pyrameter

Pyrameter is a PHPUnit extension, so there is nothing to wire into Laravel's service container — no service provider, no config publish, no facade. It hooks the PHPUnit event system and prints a shape report after the run.

Register the extension in phpunit.xml. PHPUnit 10+ replaced the old listener API with extensions, so this goes in the top-level config:

<phpunit bootstrap="vendor/autoload.php" ...>
    <extensions>
        <bootstrap class="Boundwize\Pyrameter\Extension"/>
    </extensions>
</phpunit>

Confirm the exact extension FQCN with composer show boundwize/pyrameter or the package README before committing — it is the one thing you must get exactly right, and the failure mode is silent (no report, no error).

Run PHPUnit directly, not artisan test, for the first look.

vendor/bin/phpunit

Laravel's artisan test pipes output through Collision, which reformats and can drop raw post-run output from extensions. Get a clean read first, then test whether php artisan test still surfaces the report in your setup.

First use case: baseline audit, not enforcement. Run the suite on main and read the shape line. The near-universal result for a Laravel app is the one in the README — Shape: Integration Mountain — because Laravel's default scaffolding (tests/Unit, tests/Feature) plus Eloquent-heavy domain code pushes almost everything into the Feature bucket. That is your starting number. Record it, then decide whether you care.

Where to look first, in order:

  1. The post-run ASCII summary — it tells you the detected shape, whether it passed, and the per-layer counts.
  2. How Pyrameter decided each test's layer. This is the real configuration surface, and it is the thing you will spend time on.
  3. Whether a violation affects the process exit code (see Gotchas).

Implementation Patterns

Layer assignment by convention beats layer assignment by annotation. Two viable strategies:

Directory-based — map tests/Unit → unit, tests/Feature → integration, tests/Browser (Dusk) → e2e. Cheap, zero touch on test files, and aligns with what Laravel developers already expect. The downside is that Laravel devs routinely put DB-touching tests in tests/Unit, so your "unit" count will be a lie until you clean house.

Attribute-based — annotate each test class explicitly, which forces a decision at authoring time and survives file moves. Better for mature suites, more friction day one. For a mid-size app, use convention as the default and attributes as the exception for tests that don't fit their directory.

Treat the shape as a CI artifact before you treat it as a gate. Add a dedicated script and a non-blocking job:

"scripts": {
    "test:shape": "vendor/bin/phpunit --testsuite=Unit,Feature"
}

Then in CI, run it alongside the normal suite (or as a scheduled nightly) and capture stdout into the build log or a job artifact. Only after a few weeks of data should you consider failing a build on it. A shape gate introduced on day one will be reverted on day two by whoever is shipping a feature.

Ratios, not perfection. The pyramid is a heuristic. In Laravel, request/response tests, policy tests, and job tests are legitimately integration tests, and there's often no meaningful unit-level seam without refactoring. Aim for "the unit layer is growing" and "the E2E layer is not the fastest-growing segment," rather than a fixed 70/20/10 split. Track the number across releases; a suite that stays flat while Feature grows is a suite drifting in the wrong direction.

Separate the layers you can actually isolate. If Dusk or browser tests run in a different job with a different command, Pyrameter only ever sees a fragment of the suite — the shape it reports is the shape of that invocation. Decide deliberately: either run the shape report over the full set (slow, needs a browser in the same job) or run it only over Unit+Feature and explicitly document that E2E is out of scope.

Use subsets while remediating. --testsuite=Unit alone is a fast feedback loop for "is the unit layer actually growing?" without waiting on the full run. Pair with --filter on a domain namespace if you're driving a specific module toward a healthier shape.

Pair it with coverage, not instead of it. Coverage tells you the suite touches the code; Pyrameter tells you how it touches it. A file at 100% via a single feature test is a coupling risk that coverage won't flag. Read the two reports side by side when reviewing a PR that drops a big test file in.

Put it in the PR template. A one-line "shape delta" field is the lowest-friction way to make the metric visible without making it a gate. Reviewers notice when a 400-line Feature test lands and nothing moved in Unit.

Gotchas and Tips

The exit code is the trap. Verify early whether a Result: Violated produces a non-zero exit. If it does, registering the extension can turn every existing green build red on the commit that adds the dependency. Test in a throwaway branch: add the config, run vendor/bin/phpunit; echo $?.

artisan test and the Collision printer. Laravel's test command wraps PHPUnit's output. Post-run extension output may be swallowed, reordered, or interleaved with the summary. If vendor/bin/phpunit shows the pyramid and php artisan test doesn't, that's the cause — not a broken install. Use the raw binary in CI for anything that parses or artifacts the output.

Pest compatibility is not guaranteed. Pest wraps PHPUnit and consumes the same event system, but attribute-based layer tagging is awkward in Pest's functional style (test('...', fn() => ...) has no class to annotate). If you're on Pest, lean hard on directory conventions and validate that the report appears at all before building process around it.

Parallel runs. With Paratest or PHPUnit's own process isolation, per-process extension instances each see a slice of the suite. Whether the report aggregates or you get N partial reports is worth checking if you parallelise. Until verified, run the shape report serially.

Filtered and skipped tests skew the denominator. --filter, --group exclusions, and markTestSkipped all change the counts. A green shape on a filtered run means nothing. Always measure on the same invocation you'd use for a full CI run.

Data providers: count executions or count methods? A single test method with a 40-case provider can dominate a layer's count depending on how the measurement is done. This materially changes the reported shape for suites that lean on providers. Check which it is before drawing conclusions.

Inheritance from Tests\TestCase. Attributes on a Laravel base TestCase may or may not be inherited by subclasses depending on the implementation. Be explicit on each test class until you've confirmed the behaviour — a base-class annotation that silently doesn't apply will make every test land in a default bucket.

Pin the version. This is a 9-star package with zero dependents and a single maintainer. Pin an exact version ("boundwize/pyrameter": "1.0.0" rather than ^1.0) and be prepared for the extension API or config format to shift between releases. It's a dev-only measurement tool, so a hard pin costs you nothing and protects your build.

Budget for the "no" answer. The most likely production outcome is that you run it once, discover you're an integration mountain, and either (a) accept that as correct for a Laravel app with a large Eloquent surface, or (b) use it as a nudge toward extracting domain services. Both are valid. The failure mode is treating a violated shape as a code-quality verdict and spending a sprint writing shallow unit tests around mocks to move a number.

Extension points are thin. This is deliberately a single-purpose reporter, not a pluggable framework. If you need per-team thresholds, historical trend storage, or a custom shape definition, expect to write a thin wrapper around it (parse the output, store JSON, compare to a baseline) rather than configure it. Keep that wrapper outside tests/ so it isn't itself measured.

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.
mehedimi/laravel-amp-mail
happones/laravel-vue-i18n-generator
jwerd/inertia-table
siberfx/flysystem-wasabi
hollyit/token-replace
ippanelcom/laravel-sdk
adachsoft/changelog-linter
adachsoft/collection
payerurl/telegrambot-crypto-payment-instant-payout
dskripchenko/laravel-hybrid-search
riorizkyrainey/lara-ced
mehedimi/static-vel
zharp/zharp
nonsapiens/bigquery-model-sync
lgu-bayambang-ict-office/lgu-laravel-development-standards
andydefer/laravel-support
se7enxweb/flysystem
lampwick/laravel
jeslage/statamic-emojis
micahdshackelford/rmap-laravel