levelmax World 1 Strict Types
  1. 1
  2. 2
  3. 3
  4. 4
  5. 5
  6. 6
  7. B
L
Level 1.3 ~12 min Target: PHPStan level 4

never, void, and static

Some return types aren't about values at all. They're about what happens next: whether the method comes back, whether its result means anything, and what kind of object you get to carry on with. Get them wrong and PHPStan reasons about code paths that can't happen, and misses ones that can.

never: this doesn't come back

A guard helper that always throws is common and useful:

Guard::fail("Unknown warehouse $code");

Declare it void and PHPStan has to assume it returns. Every caller then seems to fall off the end of the method without returning a value. never says the function can't finish normally: it always throws or exits. PHPStan then knows that nothing after the call runs.

void: don't use my result

void doesn't just mean "returns nothing". It's a promise to callers that there's nothing worth looking at. PHPStan reports any caller that uses the result of a void method, and from level 4 it adds that an if around one is always false.

If callers need to know what happened, the method isn't void. Give it an honest return type, like bool for "did anything change?", rather than having callers check state afterwards.

static: the class you called it on

Fluent methods usually return $this. Declared as self, the return type means the class this method is written in, even when it's called on a subclass:

(new OrderEmail())->to('sam@example.test')->withTracking();
// Call to an undefined method Email::withTracking().

PHP runs this fine, because $this really is an OrderEmail. But the signature says Email, so PHPStan only believes Email. static means "the class this was called on", which is what return $this actually returns.

Power-up

Before
<?php

final class Guard
{
    public static function fail(string $message): void
    {
        throw new LogicException($message);
    }
}

class Email
{
    public function to(string $address): self
    {
        $this->to[] = $address;

        return $this;
    }
}
After
<?php

final class Guard
{
    public static function fail(string $message): never
    {
        throw new LogicException($message);
    }
}

class Email
{
    public function to(string $address): static
    {
        $this->to[] = $address;

        return $this;
    }
}

Quick check

1. A helper always throws an exception. Which return type tells PHPStan that code after a call to it can't run?
2. Email::to() is declared : self and returns $this. You call it on an OrderEmail. What type does PHPStan think you get back?
3. A caller checks the result of a void method. What's usually the right fix?

CHALLENGE

Three small signatures are telling PHPStan the wrong story. Each one has a caller in a read-only file that's correct as written.

  • Fail::because() always throws, but Coupons looks as if it can fall off the end.
  • Basket::remove() returns nothing, but RemoveFromBasket needs to know whether anything was removed.
  • Filter::where() loses track of which kind of filter it's building, so MugSearch can't call inStock() after it.

Fix src/Fail.php, src/Basket.php, and src/Filter.php so the tests pass and PHPStan is clean at level 4.

Running on PHP 8.3…

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