Skip to Content
Course content

44: Static Analysis with PHPStan

Click on the "Edit" button in the top corner of the screen to edit your slide content.

A few years ago, I was reviewing a pull request for a payment processing module. A junior dev had changed a method in our AccountRepository to return ?User instead of User because, logically, a user might not actually exist in the database. It made perfect sense. However, they missed one tiny call to that method in a legacy reporting script. We merged it, and an hour later, the logs lit up like a Christmas tree with Fatal error: Uncaught Error: Call to a member function getEmail() on null. The script had crashed in production because we relied on a human to remember every single place that method was called.

That's the gap that static analysis fills. In a language as flexible as PHP, it's incredibly easy to assume a variable is a string when it's actually null or false. PHPStan doesn't run your code; it reads it. It builds a map of your types and logic, then screams at you before you ever hit "git push" that you're trying to call a method on something that might not exist.

Closing the Gap Between Intent and Reality

When you first install PHPStan via Composer, you'll notice it doesn't just give you a "pass/fail." It operates on levels from 0 to 9. Level 0 is basic—it catches obvious mistakes like calling a function that doesn't exist. Level 9 is "pedantic mode," where it demands absolute certainty about every single type, including mixed arrays.

I usually recommend starting at Level 5. It's the sweet spot where you catch the most dangerous bugs—like the nullable return value I mentioned—without getting bogged down in the minutiae of every single docblock. Here is a typical scenario PHPStan would catch that your IDE might miss if the types are complex:

public function getDiscountedPrice(Product $product, ?User $user): float 
{
    // If $user is null, this will crash at runtime.
    // PHPStan Level 5 will flag this immediately.
    if ($user->isPremium()) { 
        return $product->getPrice() * 0.8;
    }
    return $product->getPrice();
}

To fix this, you'd have to wrap that call in a null check. It feels like extra work in the moment, but it's the difference between a stable app and a 3:00 AM wake-up call.

Taming "Mixed" and Magic with PHPDocs

PHP's type system has come a long way, but we still deal with arrays. The biggest headache in PHP is the array type—it tells you nothing about what's inside the array. Is it a list of integers? A map of strings to objects? PHPStan allows you to use "generics-style" PHPDoc annotations to bring type safety to your collections.

I've spent too many hours debugging an array of IDs that accidentally contained a few null values. By using a specific annotation, you can force PHPStan to verify the contents of your loops:

/**
 * @param array<int, User> $users
 * @return array<int, string>
 */
public function getUserEmails(array $users): array 
{
    $emails = [];
    foreach ($users as $user) {
        // PHPStan now knows $user is an instance of User
        $emails[] = $user->getEmail(); 
    }
    return $emails;
}

If you accidentally pass an array of strings to this method, PHPStan will flag it. You're essentially creating a contract that is enforced by the tool, even if the PHP engine itself is too permissive to care.

Integrating Analysis into Your Workflow

Static analysis is useless if you only run it once a month. I treat PHPStan like a compiler. If the analysis fails, the code isn't finished. The most effective way to use it is to add it to your CI/CD pipeline or as a pre-commit hook.

One tip: don't try to jump to Level 9 on a legacy project. You will be greeted with 4,000 errors and you'll want to quit software engineering entirely. Instead, use the phpstan-baseline.neon feature. This "ignores" all existing errors in your current codebase but flags any new errors you introduce. It allows you to stop the bleeding immediately while cleaning up the old technical debt incrementally.




📋 Practical Task

Exercise: Squashing Type Errors in the OrderProcessor Service

You have been handed a OrderProcessor class that is causing intermittent crashes in production. The code is syntactically correct, so PHP doesn't complain until the code actually runs. Your goal is to use PHPStan's logic to find and fix the bugs.

The Scenario: The processOrder method takes an array of items and an optional discount code. However, the code ignores the possibility that the discount code might be invalid or that the items array might be empty/malformed.

class OrderProcessor {
    public function processOrder(array $items, ?string $discountCode): float {
        $total = 0;
        foreach ($items as $item) {
            // BUG: $item is assumed to be an object with getPrice()
            $total += $item->getPrice();
        }

        if ($discountCode) {
            // BUG: getDiscountAmount could return null if code is expired
            $total -= $this->getDiscountAmount($discountCode);
        }

        return $total;
    }

    private function getDiscountAmount(string $code): ?float {
        $discounts = ['SAVE10' => 10.0, 'WELCOME' => 5.0];
        return $discounts[$code] ?? null;
    }
}

Your Task:

  1. Rewrite the processOrder method so that it would pass PHPStan Level 6.
  2. Add a PHPDoc annotation to the $items parameter to specify that it must be an array of OrderItem objects.
  3. Implement a null check for the result of getDiscountAmount to ensure you aren't subtracting null from a float.
  4. Ensure the method handles the case where $items might be empty without returning an unexpected type.
Rating
0 0

There are no comments for now.

to be the first to leave a comment.