PHPStan reports an error, you fix it. But paradoxically, that fix actually makes your code worse. How is that possible?

I remember going through PHPStan output once, systematically fixing one warning after another. I felt productive. Code is cleaner, types are correct, green everywhere. A month later I was scratching my head over why a function was returning an empty string when it shouldn't. An hour-long detective story, yet the culprit was obvious — me and my “fix.”

One thing first, though: PHPStan does exactly what it should. It warns you that a function may return null or false, and forces you to think about it. That's great. The problem is your reaction.

An Innocent Example

Say we have a function that removes extra whitespace from text:

function normalizeSpaces(string $s): string
{
	return preg_replace('#\s+#', ' ', $s);
}

PHPStan reports: Function preg_replace returns string|null but function should return string. Sure enough, preg_replace can return null if a regex error occurs. So let's fix it, right?

function normalizeSpaces(string $s): string
{
	return (string) preg_replace('#\s+#', ' ', $s);
}

PHPStan is happy. Commit, push, done.

Except.

What You Actually Did

The original code was actually better. If preg_replace ever returned null — say, because of… (no, nothing comes to mind) — PHP would throw a TypeError. A fatal error. Tracy would light up, it would show up in the log, you'd simply know about it. (Oh right — that's why it can return null!)

After your “fix”, null silently gets cast to an empty string. The function returns "", the application keeps running, and you have no idea something went wrong. Data gets corrupted without any warning.

Congratulations, you just made your code worse 🙂

And preg_replace isn't an isolated case. Plenty of PHP functions return false or null for situations that practically never occur in normal usage — json_encode, ob_get_contents, getcwd, gzcompress, a whole range of Intl functions. Every time you reach for a type cast, stop and ask yourself: am I throwing away information about an (unlikely) error?

The Right Approach

If you want to satisfy PHPStan while preserving the original behavior, use a throw expression:

function normalizeSpaces(string $s): string
{
	return preg_replace('#\s+#', ' ', $s)
		?? throw new \LogicException('preg_replace failed');
}

This says: “I know an error can theoretically occur. If it does, I want to know.” You've essentially written out explicitly what was there implicitly before — a fatal on failure. PHPStan happy, code unharmed.

But hold on. There's an even simpler way. You can just ignore these trivial errors in PHPStan — add them to ignoreErrors in phpstan.neon or use the @phpstan-ignore annotation. And that's perfectly legitimate. After all, the original behavior where PHP throws a TypeError is basically what you want. Why change the code for that?

Even better is not having the problem at all. That's why wrappers exist — for instance, Nette\Utils\Strings::replace() wraps preg_replace and throws an exception on error. Similarly Nette\Utils\Json::encode() instead of json_encode. Use one function and the problem disappears — no null, no false, nothing to deal with.

Another option is to solve it at the PHPStan level with an extension that removes false or null from return types of selected functions. For example, nette/phpstan-rules does this for dozens of PHP functions. For regex functions, it even checks whether the pattern is a constant string — if so, it strips null from the type, since a regex error can't occur.

This is an opinionated approach, of course. And that's exactly what I like about PHPStan — it's strict, and I can customize it with extensions to suit my needs.


A silent error is worse than a loud one. And (string) is the quietest way to produce it.