PHPStan hlásí chybu, vy ji opravíte. Jenže tou opravou jste kód paradoxně zhoršili. Jak je to možné?

Pamatuju si, jak jsem jednou projížděl výstup PHPStanu a systematicky opravoval hlášku za hláškou. Cítil jsem se produktivně. Kód je čistší, typy sedí, zelená všude. O měsíc později jsem si lámal hlavu, proč mi funkce vrací prázdný string, když by neměla. Detektivka na hodinu, přitom viník byl jasný — já a moje „oprava“.
Nejdřív důležitá věc: PHPStan dělá
přesně to, co má. Upozorní vás, že funkce může vrátit null
nebo false, a donutí vás se nad tím zamyslet. To je skvělé.
Problém je až vaše reakce.
Nevinný příklad
Mějme funkci, která z textu odstraní nadbytečné mezery:
function normalizeSpaces(string $s): string
{
return preg_replace('#\s+#', ' ', $s);
}
PHPStan zahlásí: Function preg_replace returns string|null but function
should return string. No jasně, preg_replace může vrátit
null, pokud dojde k chybě v regexu. Tak to opravíme, ne?
function normalizeSpaces(string $s): string
{
return (string) preg_replace('#\s+#', ' ', $s);
}
PHPStan je spokojený. Commit, push, hotovo.
Jenže.
Co jste vlastně udělali
Ten původní kód byl ve skutečnosti lepší. Pokud by
preg_replace někdy vrátil null — třeba kvůli
… (ne, nenapadá mě proč) — PHP by vyhodilo TypeError. Fatální
chyba. Tracy by se rozsvítila, v logu by se to objevilo, prostě byste se
o tom dozvěděli. (Jo aha! Kvůli tomuto může
vrátit null!)
Po vaší „opravě“ se null tiše přetypuje na prázdný
string. Funkce vrátí "", aplikace jede dál a vy nemáte
tušení, že se něco pokazilo. Data se poškodí bez jakéhokoli
varování.
Gratuluju, právě jste kód zhoršili 🙂
A preg_replace není ojedinělý případ. Spousta PHP funkcí
vrací false nebo null pro situace, které při
normálním použití prakticky nenastanou — json_encode,
ob_get_contents, getcwd, gzcompress,
celá řada Intl funkcí. Pokaždé, když sáhnete po přetypování, zastavte
se a položte si otázku: nezahazujete tím informaci o
(nepravděpodobné) chybě?
Správné řešení
Pokud chcete uspokojit PHPStan a zároveň zachovat původní chování,
použijte throw expression:
function normalizeSpaces(string $s): string
{
return preg_replace('#\s+#', ' ', $s)
?? throw new \LogicException('preg_replace failed');
}
Tím říkáte: „Vím, že teoreticky může nastat chyba. Pokud nastane, chci o tom vědět.“ V podstatě jste explicitně zapsali to, co tam bylo implicitně předtím — fatálku při selhání. PHPStan spokojený, kód nezhoršený.
Ale moment. Ono to jde i jednodušeji. Tyhle bagatelní chyby můžete
v PHPStanu prostě ignorovat — přidat je do ignoreErrors v
phpstan.neon nebo anotací @phpstan-ignore. A je to
naprosto legitimní. Vždyť to původní chování, kdy PHP vyhodí TypeError,
je v podstatě to, co chcete. Proč byste kvůli tomu měnili kód?
Ještě lepší je problém vůbec nemít. Proto vznikají wrappery —
kupříkladu Nette\Utils\Strings::replace() obaluje
preg_replace a při chybě vyhodí výjimku. Podobně
Nette\Utils\Json::encode() místo json_encode.
Použijete jednu funkci a problém zmizí — žádný null,
žádný false, nic k řešení.
Další možnost je vyřešit to na úrovni PHPStanu rozšířením, které
u vybraných funkcí odstraní false nebo null
z návratového typu. Například nette/phpstan-rules tohle
dělá pro desítky
PHP funkcí. U regexových funkcí navíc kontroluje, zda je pattern
konstantní řetězec — pokud ano, null z typu odstraní,
protože chyba v regexu nenastane.
Je to samozřejmě opinionated přístup. A to mi na PHPStanu přesně vyhovuje — je přísný, a můžu si ho přizpůsobit rozšířením podle svého.
Tichá chyba je horší než hlasitá. A (string) je ten
nejtišší způsob, jak ji vyrobit.
Napište komentář