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

Peststan Laravel Package

mrpunyapal/peststan

PHPStan extension for Pest PHP that adds type-safe expectations, proper $this binding in test closures, generic expect() inference, type-narrowing assertions, and accurate return types for all Pest functions. Requires PHP 8.2+, PHPStan 2, Pest 3-5.

View on GitHub
Deep Wiki
Context7

Getting Started

PestStan is a PHPStan extension, so there is nothing to wire into your test runner: install it, make sure PHPStan actually analyses your tests/ directory, and run phpstan analyse as usual.

composer require --dev mrpunyapal/peststan

If phpstan/extension-installer is present (it usually is), the extension registers itself. Otherwise add the include manually:

# phpstan.neon
includes:
    - vendor/mrpunyapal/peststan/extension.neon

The most common failure on day one is that PHPStan never looks at tests, so none of the extension's value surfaces. Confirm your paths cover them:

parameters:
    paths:
        - src
        - tests

First use case to validate the install: open a Pest file that uses $this and a typed expect() chain, then run PHPStan. Before PestStan you get Cannot access property $foo on mixed and untyped expectation chains; after, $this resolves to your TestCase and expect($user) becomes Expectation<User>.

Where to look first:

  • extension.neon — confirms the registration actually happened.
  • The README feature list for expect() generics, type narrowing, $this binding, and the static analysis rules; those are the four things you will use daily.
  • src/Diagnostics/PestDiagnosticIdentifiers.php — needed the moment you start baselining or ignoring rules, because it lists both canonical and legacy identifiers.
  • Your own tests/Pest.php — the uses(TestCase::class)->in('Feature') calls are what drive per-directory $this typing.

Requirements to check before adopting: PHP ^8.2, PHPStan ^2.0, Pest ^3.0/^4.0/^5.0. Note the repository is archived (last release 2026-07-17), so treat the current version as a ceiling: pin it in composer.json and plan for the day you move to a Pest or PHPStan major that it does not know about.

Implementation Patterns

Analyse tests with a dedicated config. Keep a phpstan-tests.neon that includes the main config plus the tests path. Tests generally want a lower level and different ignore rules than src, and you do not want test-only rule noise blocking production analysis.

# phpstan-tests.neon
includes:
    - phpstan.neon
parameters:
    paths:
        - tests
    level: 5

Lean on type narrowing instead of local annotations. The extension tracks the generic parameter through narrowing assertions, so you can drop defensive assert() calls and @var blocks:

/** @var int|string $value */
$value = getValue();

expect($value)->toBeString();
// downstream $value handling is still the union, but the expectation is Expectation<string>

Structure hook-based fixture typing deliberately. The dynamic-property inference is the highest-leverage feature and the most fragile. It reads assignments in beforeEach/afterAll hooks and infers constants when possible:

beforeEach(function () {
    $this->post  = new Post;      // Post
    $this->title = 'Hello';       // 'Hello'
    $this->count = 42;            // 42
});

Factory chains and other method calls fall back to mixed, so route them through an annotated local. This is a pattern worth standardising across the team because it is not obvious from the call site:

beforeEach(function () {
    /** @var User $user */
    $user       = User::factory()->create();
    $this->user = $user;          // User
});

Rely on per-directory TestCase detection, override only where it breaks. PestStan parses Pest.php files found under your PHPStan paths and maps uses(...)->in(...) to directories. That covers the standard Laravel layout with zero config. Reach for the override when you have a non-standard layout or want one global default:

parameters:
    peststan:
        testCaseClass: App\Testing\TestCase
        pestConfigFiles:
            - tests/Pest.php

pestConfigFiles is specifically for the case where Pest.php lives outside the analysed paths. Setting testCaseClass also exposes that class's public helper methods on TestCall chains, which is handy for fluent custom helpers like it('...')->publicHelper().

Adopt the rules incrementally via identifiers. The bundled rules are opinionated and will fire on a legacy suite. Rather than a huge baseline, suppress by identifier per-rule so you keep the rest:

parameters:
    ignoreErrors:
        - identifier: pest.test.duplicateDescription
/** @phpstan-ignore pest.test.staticClosure */
it('example', static fn () => expect(true)->toBeTrue());

The pest.expectation.impossible, pest.throws.classNotFound, and pest.covers.classNotFound rules are almost always real bugs and are worth enabling first; pest.expectation.redundant and pest.test.emptyClosure are stylistic and are the ones teams typically suppress.

Wire it into CI as a separate job from Pest itself. Static analysis on tests catches the throws('App\Typo'), coversClass typos, and repeat(0) mistakes before the suite runs, and it fails fast — a few seconds versus a full Pest run.

Gotchas and Tips

The package is archived. This is the single biggest adoption risk. There will be no fixes for new Pest or PHPStan majors, and no response to false positives. Pin the version, and if you are on a bleeding-edge Pest release, verify the function return types actually resolve before committing — the README explicitly notes newer helpers are typed "when they exist in the installed Pest version."

Dynamic property inference is scoped and literal. It only sees direct assignments inside Pest lifecycle hooks. Properties set inside the test body, inside a trait, or in a hook in another file stay mixed. Multiple hooks assigning the same property produce a union (Post|Comment), which is correct but can surprise you when an assertion later looks redundant. If you see mixed where you expected a type, the assignment is almost certainly a method call — add the @var local.

beforeAll() has no $this. It runs in a static context, so $this->db = new Database; inside beforeAll is both a runtime error and a reported one (pest.lifecycle.beforeAllThisUsage). Move setup to beforeEach, or keep beforeAll for static/global bootstrap only. Relatedly, beforeAll() and afterAll() inside describe() blocks throw at runtime and are flagged statically.

Auto-detection fails silently. If $this is mixed everywhere, check in this order: is tests/ in paths? does tests/Pest.php sit inside those paths? does it use the plain uses(X::class)->in('Feature') form rather than a closure or dynamic argument? Fall back to pestConfigFiles or testCaseClass and stop debugging.

Redundant-assertion warnings are sometimes correct code. expect(true)->toBeTrue() is a legitimate smoke test in some teams' conventions. Do not contort the test to satisfy the rule — suppress by identifier, or scope the rule off for a directory via a separate config.

Low PHPStan levels blunt the extension. Type narrowing, pest.expectation.*, and the $this binding all depend on PHPStan resolving types. At level 0 with no docblocks, expectations are mixed and most rules simply never fire — which reads like a broken install. Run tests at level 5+ if you want the full benefit.

Analysis gets slower. PestStan parses Pest.php files and walks every test closure. On large suites this is noticeable in the main analysis run; the separate tests config keeps the cost out of your src job and lets you cache separately.

Treat the identifiers as an API. Diagnostics emit canonical identifiers, with legacy ones still resolvable through PestDiagnosticIdentifiers.php. If you persist ignores in a baseline or share them across projects, canonicalise to the current identifiers now — the legacy mapping is exactly the sort of shim that stops being maintained once a repo is archived.

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.
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
draftsmaninc/draftsman
square-bit/laravel-volition
himito/laravel-mailman
htinlynn/genius-helper
maco-studios/hephaestus
adachsoft/php-code-style