levelmax World 3 PHPStan to Max
  1. 1
  2. 2
  3. 3
  4. 4
  5. 5
  6. B
L
Level 3.3 ~15 min Target: PHPStan max

Extensions and framework awareness

Point PHPStan at a Laravel app without any help and it reports things like:

Access to an undefined property App\Models\Order::$reference.
Call to an undefined method Illuminate\Database\Eloquent\Builder<App\Models\Order>::unpaid().
Cannot call method orderBy() on mixed.

None of them are bugs. $order->reference is a column, which Eloquent reads through __get(). unpaid() is a scope, reached through __call(). PHPStan reads code without running it, so it can't see what a database table or a magic method will provide. These are false positives: errors about code that works.

Most teams meet a few hundred of them on the first run.

The fix that makes it worse

The quick way to a green build is to ignore them by pattern:

parameters:
    ignoreErrors:
        - '#^Access to an undefined property App\\Models\\#'
        - '#, mixed given\.$#'

That first pattern matches every undefined property on every model, including $order->totl, the typo that is about to break every invoice headline. The second hides every mixed argument in the project. Ignore patterns can't tell a false positive from a bug with the same wording, so they hide both, for good.

Extensions explain the magic

An extension teaches PHPStan a framework's rules, so it can see what the magic provides and report only what's genuinely wrong. For Laravel, that's Larastan:

composer require --dev larastan/larastan
includes:
    - vendor/larastan/larastan/extension.neon

Larastan boots your application from bootstrap/app.php, then:

  • reads database/migrations to learn every model's columns, their types, and whether they can be null
  • knows that scopes, relations and query builder calls return typed builders and collections, so Order::query()->unpaid()->get() is a Collection<int, Order>
  • understands facades, config(), app(), and the container

Run it on a project that hid its false positives and you'll see the hidden errors come back, now with only the real ones left. It does more than silence noise. delivery_note is a nullable column, so Larastan knows $order->delivery_note is string|null, which no hand-written @property tag ever said.

There are extensions for most of the libraries that rely on magic. phpstan/phpstan-phpunit understands mocks, and phpstan/phpstan-webmozart-assert understands Assert::string(). When a library makes PHPStan complain about code that works, search for its extension before you reach for ignoreErrors.

Power-up

Before
<?php

use Illuminate\Database\Eloquent\Model;

/**
 * Written by hand in 2022, before delivery_note became nullable
 * and total was renamed total_pence.
 *
 * @property int $id
 * @property string $reference
 * @property int $total
 * @property string $delivery_note
 */
final class Order extends Model
{
}
After
<?php

use Illuminate\Database\Eloquent\Model;

// No @property tags. Larastan reads every column, its type and
// whether it can be null from database/migrations, so they can't drift.
final class Order extends Model
{
    protected function casts(): array
    {
        return ['paid_at' => 'immutable_datetime'];
    }
}

Quick check

1. Without Larastan, PHPStan reports Access to an undefined property App\Models\Order::$reference. The column exists. Why does PHPStan complain?
2. A team adds - '#^Access to an undefined property App\\Models\\#' to ignoreErrors. What else does it hide?
3. Larastan knows $order->delivery_note is string|null. Where does it learn that?

CHALLENGE

This Laravel app reaches level max by ignoring anything Eloquent-shaped in phpstan.neon. The build is green, and the invoice headline is broken.

Add Larastan to phpstan.neon and delete every ignore pattern, then fix the real errors in src/Invoices/InvoiceSummary.php. An order with no delivery note should say No note for the driver.

REFERENCE

Larastan is already installed. A project includes it like this:

includes:
    - vendor/larastan/larastan/extension.neon

It boots the app from bootstrap/app.php, reads model columns from database/migrations, and understands scopes such as unpaid().

Running on PHP 8.3…

Run the tests, run PHPStan, or submit for both. Ctrl/⌘ + Enter submits from the editor.