Phpstan Analyzable Api Contracts
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.