It’s of really great help, when your APIs’ contracts are covered by static analysis.

Make PHPStan aware of your APIs’ implications️

From time to time I have to implement APIs like this:


class HttpSession {
    /**
     * checks whether the current user has a session
     */
    public function sessionExists(): bool
    {
        // implement me
        return (bool) rand(0,1);
    }

    /**
     * Returns the current user session if it exists. otherwise returns null.
     * @return UserSession|null
     */
    public function getSession(): ?UserSession
    {
        // implement me
        return rand(0,1) ? new UserSession() : null;
    }
}

class UserSession {
    // implement me
    function doSomething(): void {}
}

In this case the 2 API methods are kind of interconnected. When sessionExists returns true, getSession will return a UserSession - in other words it won’t return null.

From a API consumer point of view I can call getSession and check whether the return value is null, or use the dedicated sessionExists method.

If you use the code as shown above you will get a false positive from PHPStan though, when using the more readable sessionExists in combination with getSession:


function myController(HttpSession $session):void {
    if ($session->sessionExists()) {
        // PHPStan error: Cannot call method doSomething() on UserSession|null.
        $session->getSession()->doSomething();
    }
}

This means that I would need an additional null-check, even though from a business logic point of view this is not necessary:


function myController(HttpSession $session):void {
    if ($session->sessionExists()) {
        // no error, but unnecessarry complexity
        if ($session->getSession() !== null) {
            $session->getSession()->doSomething();
        }
    }
}

@phpstan-assert* to the rescue

As of PHPStan 1.9.0 you can give a hint about the API contract, so it knows about the implications of the API. By adding a single line of PHPDoc @phpstan-assert-if-true !null $this->getSession(), PHPStan can handle the case like you would expect.


class HttpSession {
    /**
     * checks whether the current user has a session
     * @phpstan-assert-if-true !null $this->getSession()
     */
    public function sessionExists(): bool
    {
        // implement me
        return (bool) rand(0,1);
    }

    /**
     * Returns the current user session if it exists. otherwise returns null.
     * @return UserSession|null
     */
    public function getSession(): ?UserSession
    {
        // implement me
        return rand(0,1) ? new UserSession() : null;
    }
}

class UserSession {
    // implement me
    function doSomething(): void {}
}

// ...

function myController(HttpSession $session):void {
    if ($session->sessionExists()) {
        // no error: PHPStan is aware of `getSession()` cannot return null
        $session->getSession()->doSomething();
    }
}

The added assertion tells PHPStan that $session->getSession() will not return null when used within the truethy-context of $session->sessionExists().

Here you can see the same hint at play:


function myController(HttpSession $session):void {
    if (!$session->sessionExists()) {
        return;
    }

    // no error: PHPStan is aware of `getSession()` cannot return null
    $session->getSession()->doSomething();
}

See the example at the PHPStan playground. Also, make sure you read the PHPStan release announcement which contains even more details.

Found a bug? Please help improve this article.


<
Previous Post
Phpstan Speedzember
>
Next Post
Rexfactor Introduction