PHPackages                             rasuvaeff/understudy-phpstan - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [Testing &amp; Quality](/categories/testing)
4. /
5. rasuvaeff/understudy-phpstan

ActiveLibrary[Testing &amp; Quality](/categories/testing)

rasuvaeff/understudy-phpstan
============================

PHPStan extension for the understudy test double library: matcher-aware specification closures and misuse rules

v0.1.0(today)01↑2900%BSD-3-ClausePHPPHP 8.3 - 8.5CI passing

Since Aug 25Pushed todayCompare

[ Source](https://github.com/rasuvaeff/understudy-phpstan)[ Packagist](https://packagist.org/packages/rasuvaeff/understudy-phpstan)[ Docs](https://github.com/rasuvaeff/understudy-phpstan)[ RSS](/packages/rasuvaeff-understudy-phpstan/feed)WikiDiscussions master Synced today

READMEChangelog (1)Dependencies (13)Versions (2)Used By (0)

rasuvaeff/understudy-phpstan
============================

[](#rasuvaeffunderstudy-phpstan)

[![Latest Stable Version](https://camo.githubusercontent.com/40cc92e0958845a57c503acfee0d761c151f3e2834e08ee3b04890ea69a433b0/68747470733a2f2f706f7365722e707567782e6f72672f7261737576616566662f756e64657273747564792d7068707374616e2f76)](https://packagist.org/packages/rasuvaeff/understudy-phpstan)[![Total Downloads](https://camo.githubusercontent.com/2b77e8440b2a579cf831a5dbf0327b03db675446ff8d3c233f64c2b716058c3b/68747470733a2f2f706f7365722e707567782e6f72672f7261737576616566662f756e64657273747564792d7068707374616e2f646f776e6c6f616473)](https://packagist.org/packages/rasuvaeff/understudy-phpstan)[![Build](https://github.com/rasuvaeff/understudy-phpstan/actions/workflows/build.yml/badge.svg)](https://github.com/rasuvaeff/understudy-phpstan/actions/workflows/build.yml)[![Static analysis](https://github.com/rasuvaeff/understudy-phpstan/actions/workflows/static-analysis.yml/badge.svg)](https://github.com/rasuvaeff/understudy-phpstan/actions/workflows/static-analysis.yml)[![Psalm level](https://camo.githubusercontent.com/68f7f31799f2b93c710b14ba3877072e7fe07ec9d7cee3fdf67e14beab3e1b6f/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f7073616c6d2d6c6576656c5f312d626c75652e737667)](https://github.com/rasuvaeff/understudy-phpstan/actions/workflows/static-analysis.yml)[![PHP](https://camo.githubusercontent.com/aa88c8e62f028a36d5f13498e2eaf03f6065d26de7ea6ca3f6652c8471405561/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f646570656e64656e63792d762f7261737576616566662f756e64657273747564792d7068707374616e2f706870)](https://packagist.org/packages/rasuvaeff/understudy-phpstan)[![License](https://camo.githubusercontent.com/6cb285b57819f8de0acfb34923298f4f569f962544e8fe35331da2d163f4e485/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f6c6963656e73652d4253442d2d332d2d436c617573652d626c75652e737667)](LICENSE.md)[Русская версия](README.ru.md)

PHPStan extension for [understudy](https://github.com/rasuvaeff/understudy).

> Using an AI coding assistant? Point it at [llms.txt](llms.txt).

Requirements
------------

[](#requirements)

- PHP 8.3 - 8.5
- `phpstan/phpstan` ^2.2.2
- `rasuvaeff/understudy` ^0.1

Installation
------------

[](#installation)

```
composer require --dev rasuvaeff/understudy-phpstan
```

With [phpstan/extension-installer](https://github.com/phpstan/extension-installer)that is all. Without it, include the extension yourself:

```
includes:
    - vendor/rasuvaeff/understudy-phpstan/extension.neon
```

What it does
------------

[](#what-it-does)

understudy specifies a call by making it inside a closure:

```
when(fn () => $repository->find(Arg::int(min: 1)))->returns($book);
```

Four things about that line are invisible to PHPStan on its own, and this extension is those four things.

### 1. A matcher fits whatever the contract declares

[](#1-a-matcher-fits-whatever-the-contract-declares)

`Arg::int()` is declared `mixed`, because a matcher has to be passable wherever a contract declares anything at all. At level 9 and above PHPStan reports it as `Parameter #1 $id … expects int, mixed given` — correct about the type, wrong about the code.

The extension types every matcher as `never`, the bottom type, which every parameter accepts. **Nothing is suppressed.** A wrong argument beside a matcher, a method the double does not have, the statements around the closure — all keep their reports:

```
when(fn () => $repository->rename(Arg::any(), 'not an int'));
//                                            ^^^^^^^^^^^^ still reported
when(fn () => $repository->missing(Arg::any()));
//                        ^^^^^^^ still reported
```

Below level 9 there is nothing to fix here — PHPStan does not check `mixed`against a declared parameter — and the rest of the extension works at every level.

### 2. `returns()` is checked against the method being specified

[](#2-returns-is-checked-against-the-method-being-specified)

The core declares `when(): WhenBuilder`, and it has no choice: which method is being specified is known only from the closure. The extension fills the template parameter in, and PHPStan does the rest:

```
when(fn () => $gate->open(1))->returns('yes');
// Parameter #1 ...$values of method WhenBuilder::returns() expects bool, string given.
```

### 3. `wire()` has the shape of the class it wired

[](#3-wire-has-the-shape-of-the-class-it-wired)

```
$wired = Understudy::wire(Checkout::class);

$wired['doubles']['repository'];  // Offset 'repository' does not exist on
                                  // array{books: BookRepository, clock: Clock}.
$wired['doubles']['clock']->tick();  // Call to an undefined method Clock::tick().
```

### 4. Specifications that cannot work are reported

[](#4-specifications-that-cannot-work-are-reported)

Each of these has a runtime counterpart — the engine throws, or the expectation never matches. Reporting them statically buys the one thing runtime cannot: a specification that can never match is exactly the mistake a green suite hides.

IdentifierReported when`understudy.closure`The closure specifies nothing, makes more than one call, or calls a static method a double cannot intercept`understudy.cardinality``times(5, 2)`, a negative bound, `verify(…, never: true, times: 3)`, `times` beside a `minimum``understudy.matcher`A matcher whose kind the parameter can never accept: `Arg::int()` where a `string` is declared`understudy.returns``returns()` on a method declared `void`, where no value is ever observed`understudy.matcherLeak`A matcher written outside a specification, where it reaches the code as a valueThe rules are silent whenever they are not sure. A refined parameter type — `non-empty-string`, an int range — answers "maybe" to its plain kind, and a matcher can produce a value that fits it, so nothing is reported. A false accusation costs more than a missed one here, because the engine still catches at runtime what the extension misses.

To silence one of them, use its identifier:

```
parameters:
    ignoreErrors:
        - identifier: understudy.matcherLeak
```

Why `understudy.matcherLeak` exists
-----------------------------------

[](#why-understudymatcherleak-exists)

Typing every matcher as `never` is what lets one stand in for a typed parameter, and it does so everywhere — including in a real call:

```
$repository->find(Arg::int());  // not a specification: the matcher is the argument
```

Without a rule for it the extension would be *weaker* than no extension for that mistake, because PHPStan would otherwise have reported the argument itself. Saying it directly is also better than the type error it replaces: at runtime the matcher reaches the code as a sentinel object, and the failure it eventually causes names neither the matcher nor the line.

Security
--------

[](#security)

The extension runs inside PHPStan, reads source and reflection, and reports. It executes no code from the project under analysis and writes nothing.

Examples
--------

[](#examples)

See [examples/README.md](examples/README.md). The executable demonstration is the set of fixture projects under `tests/Integration/Fixtures`, each analysed by a real PHPStan process as part of `composer build` — including a control run with the extension switched off, which is what tells a working extension apart from one that loads and does nothing.

The understudy family
---------------------

[](#the-understudy-family)

PackageWhat it is[rasuvaeff/understudy](https://github.com/rasuvaeff/understudy)The engine: doubles, matchers, expectations, verification.[rasuvaeff/understudy-testo](https://github.com/rasuvaeff/understudy-testo)Testo adapter — verification and reset around every test.[rasuvaeff/understudy-phpunit](https://github.com/rasuvaeff/understudy-phpunit)PHPUnit and Pest adapter — the same, through a trait.[rasuvaeff/understudy-psalm](https://github.com/rasuvaeff/understudy-psalm)Psalm plugin — matcher-aware specifications and misuse diagnostics.**rasuvaeff/understudy-phpstan** *(this package)*PHPStan extension — the same for PHPStan, plus its own rules.Development
-----------

[](#development)

```
make build          # validate + normalize + require-checker + cs + psalm + test + integration
make test           # unit suite
make test-coverage  # unit suite with coverage
make mutation       # mutation testing
make release-check  # build + rector + bc-check + mutation
```

License
-------

[](#license)

BSD-3-Clause. See [LICENSE.md](LICENSE.md).

###  Health Score

39

—

LowBetter than 84% of packages

Maintenance100

Actively maintained with recent releases

Popularity2

Limited adoption so far

Community6

Small or concentrated contributor base

Maturity40

Maturing project, gaining track record

 Bus Factor1

Top contributor holds 100% of commits — single point of failure

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

###  Release Activity

Cadence

Unknown

Total

1

Last Release

0d ago

### Community

Maintainers

![](https://www.gravatar.com/avatar/b0812d5572a7041dfe36e222d295b2e6dc55833a605350fcde58a51a5965ed30?d=identicon)[rasuvaeff](/maintainers/rasuvaeff)

---

Top Contributors

[![rasuvaeff](https://avatars.githubusercontent.com/u/1352718?v=4)](https://github.com/rasuvaeff "rasuvaeff (7 commits)")

---

Tags

mockmockingphpphp8phpstanphpstan-extensionstatic-analysistest-doubletestingtestingPHPStanstatic analysismockstubtest doublephpstan-extension

###  Code Quality

Static AnalysisPsalm, Rector

Code StylePHP CS Fixer

Type Coverage Yes

### Embed Badge

![Health badge](/badges/rasuvaeff-understudy-phpstan/health.svg)

```
[![Health](https://phpackages.com/badges/rasuvaeff-understudy-phpstan/health.svg)](https://phpackages.com/packages/rasuvaeff-understudy-phpstan)
```

###  Alternatives

[mockery/mockery

Mockery is a simple yet flexible PHP mock object framework

10.7k536.9M28.7k](/packages/mockery-mockery)[larastan/larastan

Larastan - Discover bugs in your code without running it. A phpstan/phpstan extension for Laravel

6.5k60.6M10.7k](/packages/larastan-larastan)[shipmonk/dead-code-detector

Dead code detector to find unused PHP code via PHPStan extension. Can automatically remove dead PHP code. Supports libraries like Symfony, Doctrine, PHPUnit etc. Detects dead cycles. Can detect dead code that is tested.

5014.2M111](/packages/shipmonk-dead-code-detector)[phpstan/phpstan-doctrine

Doctrine extensions for PHPStan

67575.0M1.6k](/packages/phpstan-phpstan-doctrine)[php-mock/php-mock

PHP-Mock can mock built-in PHP functions (e.g. time()). PHP-Mock relies on PHP's namespace fallback policy. No further extension is needed.

37020.4M140](/packages/php-mock-php-mock)[staabm/phpstan-dba

2982.6M2](/packages/staabm-phpstan-dba)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
