Ošetřování chyb

Školení, která pořádám

Programovacímu jazyku PHP se často vyčítá, že sice podporuje výjimky, ale že to je jen tak na oko, protože interní funkce výjimky téměř nevyvolávají. I když pominu, že chyby PHP se dají na výjimky snadno překládat, tak bych si s touto výhradou dovolil nesouhlasit.

Výjimky se považují za nejlepší možný způsob ošetření chyb, špatně se s nimi ale pracuje v situaci, kdy nás chyba vůbec nezajímá. Vezměme si třeba takovou funkci mysql_query – ta v případě neúspěchu vrátí false, pomocí konfigurační direktivy se dá přemluvit k vyvolání PHP chyby, kterou si následně můžeme přeložit na výjimku. Tuto funkci budeme chtít použít pro zvýšení čtenosti článku – když v této operaci dojde k chybě, tak nás to nemusí zajímat. Samozřejmě budeme rádi, když se o této chybě dozvíme z logu, ale rozhodně by neměla přerušit načítání stránky, někde se zobrazit nebo ovlivnit další operace. S výjimkami tedy bude kód mnohem složitější:

<?php
// bez výjimek
mysql_query("UPDATE clanky SET ctenost = ctenost + 1 WHERE id = " . intval($_GET["id"]));

// s výjimkami
try {
    mysql_query("UPDATE clanky SET ctenost = ctenost + 1 WHERE id = " . intval($_GET["id"]));
} catch (ErrorException $e) {
    error_log($e);
}
?>

Další funkcí, kde by výjimky podle mě byly spíše na škodu, je čtení ze souboru funkcí fread. Ta na konci souboru jednoduše vrátí false – vyvolání výjimky by kód zbytečně zkomplikovalo.

Na způsobu indikace interních chyb PHP (třeba nesprávný počet parametrů funkce nebo nesprávný typ) podle mě moc nezáleží, protože to jsou chyby, kterým se dá předcházet a které by v programu vůbec neměly být. Důležité je hlavně zpracování externích chyb.

Závěr

O ošetřování chyb začal psát před čtyřmi lety David Grudl zajímavý seriál, bohužel ho ale zatím nedokončil. Jeho postoj se od té doby ale stejně prý změnil a navíc s ním podle vlastních slov ani nesouhlasí…

Chyby indikované pomocí výjimek se nedají ignorovat. Když to uděláme, tak může program přestat pracovat. Když budeme ignorovat chyby indikované návratovou hodnotou, tak program nejspíš nějak pracovat bude i v případě, že chybu neošetříme – může to ale mít velmi nepříjemné následky.

Jakub Vrána, Dobře míněné rady, 2.7.2010, diskuse: 102 (nové: 0)

Diskuse

Martin Hruška:

Jakube, myslím, že jsi dnes narazil na přesný důvod, proč si o tobě nemyslím, že jsi ten pravý, kdo by měl ostatní poučovat o tajích programování.

Za větu "Když budeme ignorovat chyby indikované návratovou hodnotou, tak program nejspíš nějak pracovat bude i v případě, že chybu neošetříme." by se mezi "opravdovými programátory" (R) střílelo. Takovéhle věci patří mezi zoufalé bastlíře a lepiče.

Jen se děsím toho, až někdo s tvým rozpoležením bude programovat řídící systém pro jadernou elektrárnu. Něco se neprovede? Nevadí. Nedozvíme se o chybě? Nevadí. Dojde k závažnému problému s výstupem? Nevadí, však on si to uživatel přinejhorším reloadne!

Bronislav Klučka:

Jenom vidíte v textu něco, co tam není.
Logická analýza jasně říká "Pokud výjimky, program spadne, pokud bez výjimek, program poběží". Prosté oznamovací, nehodnotící věty, na kterých není nic špatného, jsou naprosto správné.
Věta "Je lepší mít program bez výjimek, protože poběží, i když se něco pokazí" je samozřejmě špatná, ale v textu jaksi není.

ikona Miloslav Ponkrác:

Předřečník má naprostou pravdu.

Normální programátor, který není prase má chyby pod kontrolou. Pokud je vyhozena výjimka a já jsem se jist, že jsem spokojen s výsledkem, i když nastane chyba, prostě odchytím výjimku daného typu, v catch nastavím prázdnou akci a no problem.

Pan Vrána bohužel namísto dobrého stylu se snaží co nejméně psavý styl – a když napíše o pár znaků méně, je to pro něj důkaz, že dělá dobře. Bohužel i pro všechny, kteří ho následují.

Pan Vrána bohužel opravdu zhusta doporučuje věci, které by dobrý programátor dělat neměl.

Bohužel, v PHP je laťka nastavená příliš nízko, ale v řadě profesionálněji laděných oborech IT by si pan Vrána nedovolil své styly moc propagovat. Zesměšnil by se.

Bronislav Klučka:

Ať čtu článek, jak čtu článek, ať se dívám na příklad, jak se dívám na příklad, zda se, že pan Vrána má výjimky pod kontrolou. O nějaké ignoraci není v článku ani slovo.
"Profesionální IT obor"... to mě vždy pobaví, děkuji :) Víte, někteří lide jsou prasata a budou prasata, ať už budou používat jakýkoliv jazyk a budou pracovat v jakémkoliv prostředí. Profesionál bude psát profesionálně i PHP.

ikona Miloslav Ponkrác:

Ale houby. Zkuste si v projektu o miliónech řádků zdrojového kódu dělat prasečiny jaké doporučuje pan Vrána. To neprojde nikomu, pokud má ten projekt rozumně odladit. A nebo projde, ale projekt se sakra prodraží, nebo se projedou hrozné prachy na následné údržbě.

Já jen nechápu panu Vránu, že opakovaně doporučuje bastlířské techniky, nejen v ošetřování chyb, ale na řadě jiných míst.

Jeho tvrdohlavost v doporučování extrémně špatných technik jen proto, aby ušetřil pár stisků klávesnici bude asi časem legendární.

ikona Jakub Vrána OpenID:

Já vyjádřím myšlenku „v některých případech je elegantnější chyby ošetřovat pomocí návratové hodnoty“ a ty si to vyložíš jako „v některých případech není třeba chyby ošetřovat“. Tak si prosím ušetři ty kecy o prasečinách.

lopata:

Tak touhle radou se doufám nikdo řídít nebude.

Výjimky jsou maximálně výhodné když programujete objektově, protože pomocí probublání můžete odchytnout výjimku na tom nejvhodnějším místě a nezavšivujete si kód kontrolami návratových hodnot. Takže v tomhle případě by jste zavolal metodu nějakou skupinu metod souvisejících se zvýšením čtenosti v bloku try, který by mohl být kilometr od toho db dotazu.

Takovéhle generalizování je naprosto nevhodné a příklad nevypovídá o ničem, protože v praxi je zejména objektová architektura aplikace mnohem složitější.

Pavel Stěhule:

Tak tady si dovolím hrubě oponovat - ignorovat chyby v jakékoliv části aplikace je čuňárna! Ve výsledku může mít uživatel jakoby běžící aplikaci, ale když se podívá do třeba do db logu, tak zjistí hromadu tiše ignorovaných chyb.

ikona Jakub Vrána OpenID:

Napiš tedy, jaký je vhodný způsob ošetření chyby, když se nepodaří aktualizovat počet přečtení článku.

Tharos:

Aktualizace může vyhodit například výjimku ala „UncriticalException“, která se na místě nemusí vůbec nijak odchytávat a může probublat relativně vysoko s tím, že nějaký handler ji nakonec odchytí a třeba jen detailně zaloguje, co že se vlastně stalo, a aplikaci nechá doběhnout.

Osobně jsem taky zastánce hojného využívání výjimek. Argument, že je pak kód aplikace zanesen jejich odchytávání, moc neberu, protože při dobrém návrhu aplikace a dobře navrženém probublávání vznikne kódu navíc úplné minimum.

ikona Jakub Vrána OpenID:

Problém je právě s tím probubláním. Za aktualizací totiž nejspíš budou následovat další příkazy, na které nepovedená aktualizace nesmí mít vliv. Takže nechat ošetření výjimky na nějaké vyšší vrstvě prostě nejde – musí se ošetřit hned, aby nezrušila další příkazy.

Tharos:

Ano, to máš samozřejmě pravdu. Ale stejně, nepřesvědčilo mě to. Souhlasím s chováním uvedené funkce fread - budiž, u ní je návratová hodnota false praktická, ale v případě dotazů na databázi mají podle mě výjimky jednoznačně navrch. Ono jde totiž také o následující: nevím, jaký přesně charakter mají Tvé aplikace, ale u mých je odhadem 90% dotazů na databázi kritických. Když se mi nepodaří získat z databáze strukturu webu, nemá smysl v běhu aplikace standardně pokračovat. Když se mi nepodaří získat hlavní obsah stránky, také nemá smysl pokračovat. Když se nepodaří objednat objednávku v e-shopu, nemá smysl pokračovat. Když se u kontaktního formuláře nepodaří uložit do databáze vzkaz, nemá smysl standardně pokračovat... Osobně se jen výjimečně setkávám z dotazy, nad jejichž úplným selháním by se dalo mávnout rukou. Proto kdybych ve svých aplikacích zrušil výjimky, paradoxně bych musel napsat snad i víc kódu - u každého kritického dotazu (kterých je drtivá většina) bych musel začít testovat návratovou hodnotu.

ikona Jakub Vrána OpenID:

Při zobrazení webových stránek je typicky kritická jediná operace – získání jejich hlavního obsahu (třeba nadpisu a textu článku nebo názvu a popisu zboží). To ošetřit musíme, ať už se používají výjimky nebo návratová hodnota.

Ostatní kritické operace jsou na tom stejně a vyžadují speciální zacházení (např. nové zobrazení formuláře s uživatelem zadanými daty), nikoliv nějaké standardní ukončení aplikace kódem 500.

Naopak chyby nekritických operací (třeba získání diskusních příspěvků nebo dalších obrázků produktu) je vhodné leda tak logovat a nikoliv jimi ukončit aplikaci. Podle mé zkušenosti se s těmito chybami v aplikacích často ani nepočítá.

ikona pavel stěhule:

Tady je ještě spíš filozofický otázka - a docela dobře rozumím tomu, že na ni není jednoznačná odpověď. Pokud mám zodpovědného provozovatele aplikace, a nejedná se o extrémně kritickou aplikace - což je většina www aplikací vyjma internetového bankovnictví, tak určitá odolnost vůči provozním chybám je na místě - i když bych ji osobně zajišťoval jinak než ignorováním chyb. Pokud je provozovatel aplikace ňouma - tak se obávám, že strategie ignorování chyb vede k provozně víc víc a víc vyhnívajícím aplikacím - tak jak to lze kolem a kolem vidět.

Pavel Stěhule:

Takhle obecně - těžko říci - v podstatě při každé chybě by mělo dojít k úklidu a ukončení aplikace, modulu (v PHP zobrazení chybové stránky ať už s přiznáním nebo nepřiznáním chyby). Je to jediný způsob, jak psát deterministicky se chovající aplikace. 

ikona Jakub Vrána OpenID:

Takže kvůli tomu, že se nám nepodařilo zvýšit informaci o čtenosti článku, bychom měli ukončit aplikaci a zobrazit chybovou stránku. To je postoj, se kterým se neztotožňuji, a proto jsem také napsal tento článek.

Rozhodně to není jediný způsob pro psaní deterministicky se chovajících aplikací. Pokud chyba běh programu nijak neovlivní a pouze se zaloguje, tak to rozhodně také je deterministické.

Pavel Stěhule:

Pokud se jedná o triviální web, tak dejme tomu - můžeš se držet strategie, pokud možno zobrazit obsah. U trochu složitějšího webu (aplikace) je riziko, že ignorovaná chyba se projeví jinak než programátor čekal - že bude mít další vedlejší efekt. Záleží jak moc robustní kód chceš napsat - pro mne a pro pár dalších, kteří začínali jako klasičtí aplikační vývojáři je cílem psát maximálně robustní kód - pro webaře je to trochu něco jiného - tam, když něco spadne, tak se až tolik neděje - když přijdou o data atd...

Bronislav Klučka:

Jau!
"Vážený uživateli, došlo k chybě při ukládání dokumentu, program bude ukončen bez možnosti zkusit dokument uložit ještě jednou".
"Vážený uživateli, při natahování stránky došlo k 3ms výpadku sítě, prohlížeč bude ukončen".
Pokud ukončujete aplikaci při každé chybě, chudáci Vaši uživatelé.
A prosím, než začnete používat cizí slova, zjistěte si, co znamenají, například takový determinizmus je předvídatelnost. Váš postup je možná předvídatelný, ale není jediný, jen nejhorší možný.

jos:

s tim by se měl vypořádat db driver, když ten dotaz spadl, tak asi nejde o 3 ms výpadek sítě

ikona David Majda:

Tak, že si napíšeš wrapper mysql_query_unchecked, který jen zavolá mysql_query, zachytí případnou výjimku a bude ji ignorovat. Cca 5 řádků kódu. Navíc budeš v místě volání hned vidět, jestli tě úspěšný výsledek dotazu zajímá (použiješ mysql_query) nebo ne (použiješ mysql_query_unchecked) - podle mě docela užitečná informace.

jos:

mě by spíš zajímalo, jestli hodláš u všech ostatních dotazů ifovat návratovou hodnotu jen kvůli tomu, že nechceš bezvýznamnej dotaz dát do try / catch, případně použít návrh Davida Majdy

Bronislav Klučka:

Od toho je v PHP zavináč...

jos:

naprosto dokonale nechápu tvojí reakci

Bronislav Klučka:

Zdá se mi to, nebo s ukončením ročníku lidé pověsili logické uvažování a schopnost analyzovat text na věšák?

Mastodont:

Nemohli byste se shodnout na formulaci "Všechny chyby je třeba ošetřit, u nekritických chyb stačí zápis údajů o chybě do protokolu" ?

Kolator:

Myslím, že záleží na typu aplikace.

Na nějakém webu s články bych vysokou *spolehlivost* neřešil, *náhodné chyby* s nízkou četností (jako jsou vypadlá spojení ap.) bych nijak speciálně neodchytával, ale nechal klidně probublat až do finálního catch, kde bych jí zalogoval a zobrazil 500 chybovou stránku.
Uživatel dá F5 a je to, na to jsme všichni zvyklí.
Snaha odchytávat má v sobě riziko, že např. při špatně napsanym catch() nebo chybné hiearchii výjimek začnu na nevhodných místech zachytávat kritické chyby.

Naproti tomu např. u aplikací typu RPC (a jakýkoliv jiných "high availability" aplikacích) má cenu zabudovat vysokou spolehlivost, aby chybové statusy vypadávaly jen při skutečně kritických chybách.

xxx:

Mrknul jsem, co je Broňa Klučka zač - koukám na reference a web http://ceskeholky.cz/ - a ejhle, vyblito hafo SQL chyb včetně dotazů... to je dobře? :)

Honza:

Jako javistovi mi připadá, že se tady řeší kontrolované x nekontrolované výjimky.
Kontrolované - občas nastávají a umím si s nima poradit a tak je zachytávám.
Nekontrolované - neměly by nastat, něco je špatně v programu, hw a nezachytávám.

Třeba chyba db spojení je pro mě nekontrolovaná => crash aplikace. Kdybych chtěl extra robustní aplikaci, tak ji zachytnu a SELECT čtu třeba z filu na serveru a ne z db.

Logují ale všechny výjimky, bez vyjímky.

ikona David Grudl:

Je pochopitelné, že člověk obhajující svůj postoj použije jako příklad něco, co mu hraje do karet. Ať už záměrně nebo podvědomě. Ale uvedený příklad s inkrementací čítače je až kulervoucí :-) Vytáhl jsi tuším jedinou myslitelnou situaci, kdy stačí chybu databáze tiše zalogovat.

ikona Jakub Vrána OpenID:

Záleží na úhlu pohledu. Vezměme si třeba tuto stránku, která pokládá následující dotazy:

1. Získání nadpisu a textu článku.
2. Získání názvů a termínů školení.
3. Výpis diskuse.
4. Zjištění celkového počtu článků.
5. Výpis aktuálních pracovních pozic.
6. Seznam skupin spolu s počtem článků.
7. Výpis výběru článků.
8. Výpis nejnovějších článků.
9. A ještě nějaký ten UPDATE.

Z těchto devíti dotazů je kritický pouze první. Když selže, chci zobrazit chybu 500. Když selžou ostatní, tak rozhodně nechci, aby to ovlivnilo zbytek dotazů. Takže při ošetření pomocí výjimek je nemůžu uzavřít do jednoho try-catch, ale musím uzavřít každý zvlášť. To umožňuje zobrazit nějakou chybovou hlášku, to ale samozřejmě jde i při vracení pravdivostní hodnoty. Tato chybová hláška navíc dává smysl pouze u diskuse (bod 3), ostatní dotazy se mohou chovat stejně jako v případě, že databáze nevrátí nic.

Praxe je navíc taková, že se nikomu těch v tomto případě devět try-catch nechce psát, čehož důsledek při ošetření pomocí výjimek je ten, že stránka kvůli zcela nepodstatnému problému skončí chybou 500.

ikona pavel stěhule:

Tohle je tvůj web - na jiném portále může být kritické zobrazení reklamy. Co je kritické, a co není? O tom rozhodne kdo, programátor, kterému se nechce psát 10 řádek kódu navíc. Kritické aplikace se píší jinak - předně se nadefinuje tzv. bezpečný stav, a v případě chyby se aplikace překlápí do něj. Ale tahle vlastnost musí být definovaná v návrhu aplikace, případně v návrhu modulu. A je to cílená vlastnost - nikoliv vedlejší důsledek lenosti programátora - naopak - u složitějších systémů dá docela dost práce to navrhnout dobře.

ikona Jakub Vrána OpenID:

Je celkem nepodstatné, jestli kritičnost operace posoudí líný programátor nebo uvědomělý analytik. Můj příspěvek pojednával o tom, kolik těch kritických operací na webových stránkách bývá. David ve svém příspěvku tvrdí, že všechny kromě jedné. Já jsem naopak na příkladu konkrétní stránky ukázal, že zcela kritická je jedna z devíti a částečně kritická ještě jedna. Chyby ostatních operací nejsou pro uživatele podstatné – jsou podstatné pro programátora, který se o nich dozví z logu a nikoliv z toho, co se zobrazí na stránce.

ikona pavel stěhule:

Jasně v tom máš pravdu - ale takhle uvažovat o návrhu si může dovolit minimum provozovatelů - ty v jedné osobě sdružuješ programátora, provozovatele i majitele aplikace. V tomto případě prakticky nehrozí, že by po tobě někdo kód přebíral nebo, že by změnila majitele.

ikona Jakub Vrána OpenID:

Ale vůbec ne. Já jsem popsal, jak by tato stránka měla fungovat z pohledu zadavatele. Bodem 1 a 3 je vhodné obtěžovat i uživatele, ostatními body pouze programátora. Že jsem vedle zadavatele zároveň i realizátorem, s tím nijak nesouvisí.

ikona pavel stěhule:

Asi si nerozumíme, ty nebereš v potaz, že jsi netradiční zadavatel - minimálně pro řadu komerčních provozovatelů bude naopak zásadní inkrementace návštěvnosti a zobrazení reklamy - jinak jaká je pravděpodobnost, že ti projde bod 1 a neprojdou ti ostatní? Navíc u většiny výše uvedených můžeš použít posloupnost: aktualizuj cache, zobraz stávající cache, zobraz nedostupnost části obsahu - nikde tam nevidím prostor pro ignorování chyby - např u zobrazení diskuze - pokud nelze zobrazit diskuzi, tak bys měl korektně zobrazit něco "diskuze není momentálně přístupná" a nikoliv tiše ignorovat problémy se serverem a pouze je logovat.

ikona Jakub Vrána OpenID:

Podle tvého názoru by tedy např. Google měl na jakékoliv hledání zobrazit chybu 500, pokud by se nepodařilo načíst AdWords? Vždyť to je nesmysl.

Jak už jsem detailně vysvětlil, tak nutnost reportování uživateli vidím pouze u bodu 1 a vhodnost ještě u bodu 3. Reportování chyb uživateli u ostatních bodů je spíše na škodu. Nedává mi smysl, aby třeba v bočním menu bylo „Omlouváme se, ale výběr článků se nepodařilo načíst“ (což se mimochodem zaindexuje vyhledávači) – to uživatele nemusí zajímat. A už vůbec by to nemělo způsobit fatální chybu. S tímto stavem lze z pohledu uživatele pracovat stejně, jako kdyby žádný výběr článků neexistoval.

Překvapuje mě, že mluvíš o robustních aplikacích a přitom se ptáš na pravděpodobnost. To skoro zavání tím, že třeba s chybou v bodě 7 vůbec samostatně nepočítáš (což je běžná praxe). Důsledkem při použití výjimek je fatální chyba, při použití návratové hodnoty pouze záznam v logu.

ikona pavel stěhule:

Například u diskuzí pod článkem - dejme tomu, že přijdu, vidím článek a pod ním neuvidím diskuzi - tak už nemám důvod se k článku vracet - pokud tam uvidím hlášku, že momentálně nelze diskuzi zobrazit, tak se tam možná vrátím - jinak souhlas - robotu nemá cenu zobrazovat chybové hlášky - ale např. pokud mám nějaká data nedostupná, tak je lepší rovnou dát najevo, že je systém mimo provoz, nebo vrátit nějaké minimum? S tou pravděpodobností - podle mne je docela pravděpodobné, že když načteš články, tak načteš i zbytek - a nemá cenu proto zbytek ošetřovat jinak - ledaže bys měl data rozdělená v různých SQL serverech.

Jinak asi se nedohodnem - tebe děsí, že by uživatel mohl mít nedostupný systém, mne děsí fakt, že bych neměl chyby systému pod kontrolou.

ikona Jakub Vrána OpenID:

Nechápu, proč pořád zmiňuješ diskuse (bod 3). O těch jsem už v první odpovědi napsal, že chybu v nich je vhodné reportovat uživateli (ale ne jako fatální). Pak jsem ti to ještě jednou zopakoval v příspěvku, na který reaguješ.

Můj přístup rozhodně neznamená, že chyby nejsou pod kontrolou. Jen je potřeba rozlišovat chyby významné pro uživatele a chyby významné pro programátora.

Která ze tří reakcí na nekritickou chybu je pro uživatele nejlepší?

A. Omlouváme se, ale výsledky vyhledávání nelze zobrazit. Příčinou je, že se nepodařilo načíst reklamy.
B. Omlouváme se, ale nepodařilo se načíst reklamy (vedle výsledků).
C. (samotné výsledky bez reklamy)

Já tvrdím, že C, ty se zřejmě kloníš k A (i když možná s míň explicitní chybovou hláškou).

ikona David Grudl:

> Z těchto devíti dotazů je kritický pouze první.

Hele ty moc fandíš svým článkům, ve skutečnosti je kritický pouze ten třetí :-))

Ale vážně. Každý dotaz je kritický v rámci svého kontextu. Pokud se nepodaří načíst názvy a termíny školení, musí selhat celé renderování DIVu s titulkem "Školení, která pořádám". Obdobně to platí pro další body, ať už jde o diskuse nebo zmíněný bod č.1 - ano, tam v případě chyby vrátím HTTP kód 500. Jen mírně jinačí obsluha téže výjimky.

ikona Jakub Vrána OpenID:

Ale vždyť jsem psal, že u ostatních výpisů (konkrétně 2, 6, 7, 8) může být chování stejné jako v případě, kdy se nepodaří načíst žádná data. A pokud se shodneme na tom, že žádná data (tedy např. prázdný výběr článků) není důvod pro výjimku (což by znamenalo řízení běhu programu pomocí výjimek), tak to na myšlence článku nic nemění.

<?php
// s výjimkami
try {
  $best = getBest();
  if ($best) {
    // tady bude výpis
  }
} catch (
Exception $e) {
  // prázdný blok
}

// s návratovou hodnotou
$best = getBest();
if (
$best) {
  // tady bude výpis
}
?>

ikona David Grudl:

Zeptám se jinak - proč by getBest() měla vyhazovat výjimku? (tedy za předpokladu, že píšu kód pomoci výjimek).

ikona Jakub Vrána OpenID:

getBest() vyhodí výjimku proto, že klade databázový dotaz, který skončí chybou. Samozřejmě by si výjimku mohla sama zpracovat, to ale na situaci nic nemění – prázdnému bloku catch se nevyhnu. Navíc by to ani nebylo vhodné: Pokud bych měl stránku, která by sloužila pouze k výpisu výběru článků, tak by to bylo důvodem pro fatální chybu.

ikona David Grudl:

Jak bys tedy implementoval getBest() na stránce sloužící pouze k výpisu výběru článků bez použití výjimek?

ikona Jakub Vrána OpenID:

Pokud slovem „implementoval“ myslíš „použil“ (implementace bude jen jedna), tak takhle:
<?php
$best
= getBest();
if (
$best === false) {
  // 500
} elseif (!$best) {
  echo "Zatím nic.";
} else {
  // normální výpis
}
?>

ikona David Grudl:

Jasně. "implementoval getBest() použitelnou pro protřeby stránky sloužící pouze..." Tedy jak by vypadalo getBest()

ikona Jakub Vrána OpenID:


<?php
function getBest() {
  return dbResult("SELECT * FROM article WHERE best");
}

function
dbResult($query) {
  $result = mysql_query($query);
  if (!$result) {
    return false;
  }
  $return = array();
  while ($row = mysql_fetch_assoc($result)) {
    $return[] = $row;
  }
  return $return;
}
?>

ikona David Grudl:

Srovnej s použitím výjimek:

<?php
function getBest() {
  return mysqli_query(..., "SELECT * FROM article WHERE best")->fetch_all(MYSQLI_ASSOC);
}

...

try {
  $best = getBest();
  if ($best) {
    // normální výpis
  } else {
    echo "Zatím nic.";
  }
} catch (
DbException $e) { // čitelnější než mlhavé false
  // 500
}
?>

ikona Jakub Vrána OpenID:

Ano, v tomto případě je kódu zhruba stejně. Ale v případě nefatálnosti chyby je kódu s výjimkami prostě víc, navíc je podstatně ošklivější:

<?php
try {
  $best = getBest();
  // ...
} catch (Exception $e) {
}
?>

A přesně o tom je tento článek.

ikona David Grudl:

Kódu je zhruba polovina.

ikona Jakub Vrána OpenID:

Kterého kódu je polovina? Hlavní kód if-else má 8 řádek, try-catch 10. getBest() má v obou případech 1 řádek.

Pokud chceš do součtu započítat i dbResult(), tak si prosím uvědom, že jde o jednu funkci společnou pro celý projekt (potažmo pro všechny projekty). A hlavně dopiš svůj getBest() tak, aby vůbec dokázal výjimku vygenerovat. Když v něm teď dojde k chybě, tak skončí fatální chybou. A taky nezapomeň dodefinovat DbException.

ikona David Grudl:

No když už počítáme takové nesmysly jako počet řádků demokódu, tak do něho chci započítat i dbResult(). Pokud mi nezapočítáš dbResult, já ti nezapočítám getBest() !

;)

ikona Jakub Vrána OpenID:

Je poněkud demagogické počítat knihovní funkci (které ty by ses taky nevyhnul, pouze jsi ji "zapomněl" napsat).

ikona David Grudl:

Celá tahle diskuse o počítání řádků je nesmyslná. Doufám, že ji nebereš vážně.

Kterou funkci jsem "zapomněl" napsat?

ikona Jakub Vrána OpenID:

Já tuto diskusi moc nechápu. V souladu se skutečností napíšu, že je kódu zhruba stejně, a ty zcela nesmyslně oponuješ tím, že je kódu polovina. Od té chvíle se to stává nesmyslem.

Co jsi "zapomněl" napsat, zjistíš, když si zkusíš spustit svoje getBest(), nejlépe s chybou. Ten kód je prostě nefunkční, mysqli_query() totiž v případě chyby vrací false.

ikona David Grudl:

22 versus 13

<?php
1.
function getBest() {
2.  return dbResult("SELECT * FROM article WHERE best");
3. }

4. function dbResult($query) {
5.  $result = mysql_query($query);
6.  if (!$result) {
7.    return false;
8.  }
9.  $return = array();
10.  while ($row = mysql_fetch_assoc($result)) {
11.    $return[] = $row;
12.  }
13.  return $return;
14. }
15. $best = getBest();
16. if ($best === false) {
17.  // 500
18. } elseif (!$best) {
19.   echo "Zatím nic.";
20. } else {
21.  // normální výpis
22. }
?>

<?php
1.
function getBest() {
2.   return mysqli_query(..., "SELECT * FROM article WHERE best")->fetch_all(MYSQLI_ASSOC);
3. }

...

4. try {
5.   $best = getBest();
6.   if ($best) {
7.     // normální výpis
8.   } else {
9.     echo "Zatím nic.";
10.   }
11. } catch (DbException $e) { // čitelnější než mlhavé false
12.   // 500
13. }
?>

ikona Jakub Vrána OpenID:

Jak jsem psal – zkus si prosím nejdřív svoje getBest() spustit. Nejlépe v situaci, kdy v něm dojde k chybě. Než budeš dál reagovat, tak si nejprve přečti, na co reaguješ.

ikona David Grudl:

> Ten kód je prostě nefunkční, mysqli_query() totiž v případě chyby vrací false.

Sorry, zapomněl jsem dodat, že mysqli_query používám v režimu, kdy v případě chyby vyhodí mysqli_sql_exception (via mysqli_report). Bral jsem to jako samozřejmost, když obhajuji výjimky před testováním návratového kódu.

ikona Jakub Vrána OpenID:

No dobře, teď už to alespoň dává smysl. Pořád ale porovnáváš hrušky s jabkama, protože započítáváš i knihovní funkci (která by se navíc dala při použití MySQLi o 4 řádky zkrátit). Kvízová otázka – kolik funkcí jako je getBest() (mám na mysli třeba getRecent(), …) by bylo potřeba, aby se tvůj kód stal delší než můj?

Zkrátka tvrzení, že kódu je zhruba stejně, odpovídá realitě a překvapuje mě, že máš zapotřebí s ním polemizovat.

ikona David Grudl:

Polemizovat o délce kódu mi skutečně přijde hloupé a sám se sobě divím, že jsem tak dlouho tu hru hrál ;)

Skutečně není o čem polemizovat. Pokud srovnávám techniku, která vyžaduje po každé operaci ověřit návratový kód s technikou, která to provádí centrálně, tak už od dvou operací je ta druhá technika snazší. To je přece triviální fakt.

Dále je tu fakt, že ta druhá technika vylučuje, že bych opomenul návratový kód zkontrolovat a ošetřit. Což je snad ještě důležitější, přece bezpečnost > snadnost.

Takže zatímco já mohu psát:

a();
b();

nebo

c(d());

nebo

x()->y();

ty musíš otrocky po každém kroku ověřovat návratovou hodnotu. Neberu ti to, ale přínos v tom nevidím.

ikona Jakub Vrána OpenID:

„Pokud srovnávám techniku, která vyžaduje po každé operaci ověřit návratový kód s technikou, která to provádí centrálně“ – z tohoto tvrzení mám pocit, že jsi článek a tuto diskusi nečetl příliš důkladně. Já jsem poukázal na to, že s výjimkami naopak v některých případech musíš psát kód ve tvaru:

<?php
try {
  a();
} catch (
Exception $e) {
}
try {
  b();
} catch (
Exception $e) {
}
?>

Kdežto bez výjimek stačí ono jednoduché <?php a(); b(); ?>.

Dochází k tomu v situacích, kdy a() i b() produkují nefatální (lokální) chyby, které není potřeba nijak zvlášť ošetřovat. Ne všechny chyby jsou zkrátka globální a musí vést k ukončení aplikace. V tomto vlákně jsem poukázal na to, že lokálních chyb je celá řada a globálních bývá naopak jen pár: http://php.vrana.cz/osetrovani-chyb.php#d-10326.

ikona David Grudl:

Tak to musím znovu zopakovat, že mícháš různé věci, tj. výjimky a vyskočení neošetřené výjimky. Odstraň si rovnítko mezi výjimka a "globální chyba vedoucí nevyhnutelně k ukončení aplikace".

ad bez výjimek stačí ono jednoduché a(); b() - to je na delší odpověď http://phpfashion.com/programatori-chyby-neignoruji

ikona Jakub Vrána OpenID:

Jak už jsem psal, tak s „vyskočením neošetřené výjimky“ ve smyslu fatální chyby nikde nepočítám. Naopak předpokládám, že když někdo používá výjimky, tak má nějaký jejich globální zachytávač (jako Nette).

To ale nic nemění na situaci, že když lokální chybu neošetřím lokálně, tak chyba ovlivní i ostatní příkazy, které s ní nijak nesouvisí. Prázdným blokům try-catch se tedy nevyhnu. Jinak riskuji povýšení lokální chyby na globální, jak jsem ilustroval v příspěvku http://php.vrana.cz/osetrovani-chyb.php#d-10335.

ikona David Grudl:

Globální zachytávač je z pohledu této diskuse totéž, jako vyskočení neošetřené výjimky a obojí s ní souvisí velice krajově. A pojmy lokální/globální chyby celou věc zbytečně komplikuješ.

Ty se totiž u výjimek obáváš, že nějakou z nich můžeš neošetřit a to povede k pádu aplikace. Ano, jistě, to se stát může a stává; v Nette se uživateli zobrazí chybová stránka s kódem 500, výjimka se zaloguje a odešle upozornění programátorovi.

A tady přichází (!) to klíčové: zobrazení stránky 500 je řádově MENŠÍ zlo, než poškození dat. Tj. to, co by mohlo nastat, pokud program bude "tak nějak pracovat dál". Viz příklady na http://phpfashion.com/programatori-chyby-neignoruji

Programy se nejčastěji skládají ze sekvencí příkazů, kde se vyžaduje úspěšné provedení každého z nich v daném pořadí. A je snadnější a hlavně bezpečnější takový blok napsat bez nutnosti v každém kroku testovat návratovou hodnotu.

Pokud bych totiž návratovou hodnotu správně neotestoval, nejen, že by to mohlo způsobit problém, ale také bych jej mnohem hůř odhaloval.

ikona Jakub Vrána OpenID:

Máš pravdu, že programy se skutečně nejčastěji skládají z příkazů, které se musí provést všechny a v daném pořadí. Nicméně webové stránky se nejčastěji skládají z komponent, které jsou na sobě naopak většinou nezávislé a ovlivňovat by se vzájemně neměly. Při použití výjimek je nutné každou z těchto komponent ošetřit zvlášť, což je obzvlášť nepohodlné v situaci, kdy celý kód pro zobrazení komponenty sestává z jediného příkazu.

Zobrazení stránky 500 může být dramatický problém, pokud vede k ušlému zisku nebo nasranosti uživatelů. Třeba z těch devíti bodů nemůže žádný vést k poškození dat, zato všechny by vedly k ušlému zisku a nasranosti uživatelů, pokud by vyvolaly chybu 500.

ikona David Grudl:

Ale přece nikde nepíšu, že zobrazení 500 není problém; porovnávám 500 s problémem poškození dat, navíc hůře odhalitelným. Nepolemizuj prosím s něčím, co jsem nepsal.

Absolutně nechápu souvislost mezi použitím výjimek a komponentami webové stránky. Použití výjimek souvisí se stylem programování a je přece jedno, jestli je výsledkem webová stránka nebo aplikace do iphone. To, že jsou jednotlivé komponenty nezávislé a selhání jedné nevede k selhání jiné, lze naprogramovat jak s výjimkami tak bez nich. V tomto snad ani nelze hledat argumenty pro jedno či druhé řešení, protože se pohybujeme na jiné vrstvě aplikační logiky.

Pokud máš pocit, že v konkrétním případě ti kód napsaný pomocí výjimek připadá delší, může to tak být, takové případy samozřejmě jsou, ale stejně tak je možné, že jsi ho jen navrhl neefektivně. Tohle mi i vyplývá z věty "při použití výjimek je nutné každou z těchto komponent ošetřit zvlášť", protože to znamená, že sis navrhl API nešťastně, tedy tak, abys musel každou komponentu ošetřovat. Tak si to uprav, ať ti komponenty výjimky nevyhazují a budeš happy ;)

ikona David Grudl:

> nikomu těch v tomto případě devět try-catch nechce psát, čehož důsledek při ošetření pomocí výjimek je ten, že stránka kvůli zcela nepodstatnému problému skončí chybou 500.

Mícháš dohromady tři různé věci: výjimky, vyskočení neošetřené výjimky a HTTP kód 500.

Neošetřená výjimka, která položí aplikaci, je mezní situace, která nesmí nastat. Používat ji jako argument je cca totéž, jako argumentovat parse errorem. Nad tímhle prostě nediskutujme.

HTTP kód 500 je jedna z variant ošetření chybového stavu, jejíž užití je v rukou programátora. A to ať už používá výjimky nebo ne.

ikona Jakub Vrána OpenID:

Nemíchám různé věci, pouze předpokládám, že když někdo neošetří výjimku přímo na místě, že má alespoň dost slušnosti ošetřit ji na nejvyšší úrovni tak, aby vyvolala chybu 500 (jako to dělá Nette). Neošetřená výjimka tedy nevyskočí (u té by PHP ostatně ani žádný chybový kód neposlalo) a nikde ji jako argument nepoužívám.

Proki:

V 99,99% se buď vykoná úspěšně všech 9 příkazů a nebo ani jeden. Tudíž nemá smysl ošetřovat každý příkaz pomocí try catch, ale stačí ošetřit pouze ten první nejdůležitější (respektive uzavřít vše do jednoho bloku). To že je pravděpodobnost 0,01% že ojediněle jeden z příkazů neproběhne je minoritní a podle mého názoru vůbec nevadí, když se v tu chvíli vyvolá chyba 500. Uživatel stiskne F5 a pokračuje dál jakoby se nic nedělo.

A pokud jde například o komponentu zobrazující reklamy, tak ta přeci by měla být ošetřena sama a v případě chyby buď vypsat nějakou hlášku a nebo nic nezobrazit, a ne abych v každém místě aplikace, kde tu komponentu použiji ošetřoval, jestli v ní nedošlo k nějaké chybě.

Programuji nebo programoval jsem i v JSP a ASP.NET, tam se to výjimkami jen hemží. Např i každé konvertování řetězce na číslo je potřeba ošetřit. O desktopových aplikacích vůbec nemluvím, tam stačí maličkost a už máme fatal. Jen PHP se vždy musí chovat jinak.

Jednoznačně jsem tedy pro výjimky a souhlasím s Davidem.

ikona Jakub Vrána OpenID:

Tak přesně tomuhle říkám bastlení já. „Pokud k chybě nedochází dostatečně často, tak se na ni prostě vyserem a zobrazíme pětistovku.“ A že k chybě může dojít třeba proto, že se poškodí databázová tabulka nebo ji někdo omylem smaže, takže pak k chybě dochází při každém dotazu, tě asi netrápí.

Zkrátka když napíšeš <?php dibi::query("UPDATE ... ctenost = ctenost + 1"); ?> aniž bys ošetřil výjimku, ke které tam může dojít, takže riskuješ ovlivnění ostatních příkazů, tak jde prostě o bastlení. Ale hlavně že máš dobrý pocit z toho, že pro ošetření chyb používáš ty jedině správné výjimky místo ubohé návratové hodnoty a zalogovaného varování.

Proki:

Že někdo omylem smaže tabulku? To si snad děláte srandu. Zkušený programátor ji jen tak nesmaže, technická podpora na hostingu také ne a v tom případě je tedy někde v aplikaci velká bezpečnostní díra. Navíc i díky tomu, že by v databázi chyběla jedna jediná tabulka, tak selžou ostatní více či méně důležité dotazy využívající spojení tabulky (JOIN) s touto smazanou.

Nikde jsem neřekl, že návratové hodnoty jsou ubohé. Také je využívám a to poměrně často. Ale ve zde uváděných příkladech bych určitě využil výjimky, jelikož každá chyba nějak související s databází je fatální.

ikona Jakub Vrána OpenID:

Jasně. Takže pokud se poškodí jediná databázová tabulka, do které se něco loguje, tak jde o fatální chybu aplikace. Nebo pokud lze z tabulky i nadále číst, ale nejde do ní zapisovat (třeba proto, že dojde místo na disku), opět jde o fatální chybu aplikace.

Já už jsem si obrázek udělal. Z tvých dvou příspěvků vyplývá, že jednotlivé chyby lze při komunikaci s databází ignorovat (ošetřit hromadně), protože jsou všechny fatální.

Proki:

Riziko, že se poškodí databázová tabulka a shodou okolností to bude zrovna ta nedůležitá logovací, která je pro chod systému víceméně nedůležitá je naprosto minimální.

Tím nechci nijak říkat, že jednotlivé chyby ignoruji, ale např. výše zmíněnou aktualizaci čtenosti článku bych zvýšil v metodě vracející daný článek a nikoliv až na stránce, kde daný článek vypisuji. Títmo budu mít try catch blok související s touto výjimkou pouze v této metodě a už se o nic nemusím starat. Tato výjimka mě tudíž nijak neobtěžuje a nebude zpracována rozsáhlejším try catch blokem ve stránce, který načítá článek, komentáře a třeba navigaci.

ikona David Grudl:

Jakube, i tohle je přístup - napsat aplikaci tak, že jakýkoliv neúspěch DB operace znamená rovnou 500 je otázkou jejího návrhu a _tvrdosti_ ošetření operace. Pokud je to korektní a zamýšlené chování, proč to nazývat bastlením?

Člověk časem může zkušenost prohloubit (např. narazí na hostingy, kde občas dochází místo na disku) a chování zmírní s tím, že chybu při update čítače bude jen logovat. A třeba mu ta myšlenka dojde právě při studování logu. Ale opět je to úprava návrhu.

Ve finále může stránku rozdělit do nezávislých komponent s vlastní obsluhou chyb - zalogování, vypsání informace pro uživatele, uvedení meta hlavičky noindex (děláš to?). Tohle třeba nedělám z prostého důvodu: za roky co programuju webovky jsem nikdy z logu nevyčetl, že by DB havarovala až při získávání méně důležitých dat. Nicméně věřím, že můžeš mít jinou zkušenost a pak je rozdělování na nezávislé komponenty opodstatněné. V mém případě by to nebylo nehospodárné.

V čem ale spočívá celá podstata diskuse je to, že mysql_query je natolik low-level funkce, že její návratovou hodnotu ignorovat prostě nelze. Ve většině případů je na ni přeci navázáno volání dalších funkcí, neoddělitelně, zejména fetchovací funkce. V případě unbuffered dotazů může leccos selhat i při tahání dat. A proč bych měl otrocky v každém dílčím kroku kontrolovat situaci, když to mohu udělat na vyšším levelu jedním catch.

Vtipné je, že ani ty sám návratovou hodnotu mysql_query neignoruješ, například ve funkci dbResult().

ikona Jakub Vrána OpenID:

Tvrdím snad někde, že návratová hodnota je od toho, aby se ignorovala? Pouze říkám, že její použití je za některých okolností elegantnější než použití výjimek.

Co máš na mysli uvedením meta hlavičky noindex?

ikona David Grudl:

To netvrdíš, ale v důsledku k tomu svádíš.

Hele já bych to s tebou vůbec neřešil, kdybych neznal, jak pečlivě zvažuješ možná řešení v jiných situacích a uvádíš výhody i nevýhody toho či onoho přístupu. Vedle toho je tento článek doslova tendenční, až demagogicky vystavěný. Chybí protipříklad, kdy absence kontroly návratové hodnoty může způsobit nekonzistenci dat nebo rovnou poslat PHP do kytek. Ukažuješ jen "eleganci" řešení bez výjimek, které zdánlivě nemá chybu, ačkoliv mysql.trace_mode může být opruz kvůli uvolňování result setů, nemluvě o nastavení direktiv.

---------

Stránka, která je nekompletní, by se v té podobě raději neměla indexovat.

ikona Jakub Vrána OpenID:

Co se toho meta noindex týče, tak jednak to je trochu sporné (je lepší zastaralý kompletní obsah nebo aktuální s absencí informace, která s hlavním obsahem třeba ani nijak nesouvisí?) a jednak je na jeho poslání často už pozdě (protože už jsme ukončili <head>).

ikona Jakub Vrána OpenID:

Vidíš a právě o tom článek možná byl. Já říkám, ať lidi nezachází se všemi chybami rovnou jako s fatálními, i když se jim třeba zdají nepravděpodobné. A říkám, že šikovný způsob zpracování nefatálních chyb je návratová hodnota a zalogované varování PHP. A paradoxně jsem nařčen z bastlířství právě proto, že elegantním způsobem ošetřuji nefatální chyby.

<?php
$best
= getBest();
$recent = getRecent();
$jobs = getJobs();
// ...
?>

Tento kód má správně ošetřené chyby, pokud každá z funkcí případnou chybu zaloguje a vrátí false. Naopak nemá správně ošetřené chyby, pokud funkce vyhodí výjimku, protože to ovlivní i operace, které s funkcí nesouvisí.

ikona Jakub Vrána OpenID:

Přijde mi legrační, že pojmenuješ článek „Programátoři chyby neignorují“ a potom tady obhajuješ právě jejich ignorování. Protože pokud lokální chyba ovlivní i operace, které s ní nijak nesouvisí, tak to je jen forma jejího ignorování.

Samozřejmě se dá poukázat na záměr v návrhu aplikace, pak to ale vede k absurdním situacím toho typu, že nedostupnost reklamy zapříčiní nezobrazení výsledků vyhledávání: http://php.vrana.cz/osetrovani-chyb.php#d-10335.

ikona Pavel Ptáček:

Jenom jednu poznámku - ano, mysql_query je low-level. Na druhou stranu, většina (?) lidí používá např. PDO, dibi, doctrine, etc. čili teoreticky: řešit návratovou hodnotu (!) a vyhazovat vyjímku je záležitost příslušné abstrakce.

Ani přístup PDO->query mě nevytáčí, neb se prostě udělá:
<?php
$result
= $pdo->query(..);
if(
$result) {
  $this->assign('...', $result->fetch());
}
?>

vč. případné 500 pokud to je zrovna fatální.

Problém _není_ v tom, že by mysql_query byla "příliš low-level". Problém imho je v tom, jestli právě _kvůli low-level funkcím_ překládat na vyjímky i _všechny_ ostatní funkce. A to by se IMHO dělat nemělo, protože ne všechny warningy náležitě musí znamenat vyjímku, kterou je třeba ošetřit. Pokud ano, můžete kdykoliv přejít od PHP k ASP.NET.

Syky01:

Netvrdil bych, že smazání či poškození tabulky je nemožné. Naštěstí se to nestalo na ostré databázi e-shopu, ale "jen" u klienta na počítači, ale přesto se stalo, že v Mysql zmizela asi polovina souborů v DB, takže přes phpmyadmin většina tabulek se hlásila jako view...

Ano běželo to na WAMPu, ale stalo se to.

Proki:

Jako bastlení mě to nepřijde (výše jsem zapomněl zmínit, že veškeré chyby loguji a během pár minut se mi odesílají i na email). Pokud se z ale nějakého důvodu stane, že by se mi podařilo třeba načíst jen nadpis článku a jeho text a nebyla načtena navigace, komentáře atd, tak mi přijde rozumnější ohlásit, že stránka je momentálně nedostupná. Uživatel může přijít za chvíli a to už s velkou pravděpodobností funkční bude. Pokud by uživatel viděl na stránce jen text článku a kolem nic, tak ho už na mém webu nemusí nic zajímat a s vysokou pravděpodobností se už ani nikdy nemusí vrátit.

Šaman:

Nechci přilévat oleje do kotliku, ale připadá mi, že se bavíte každý (Jakub a David) o něčem jiném.

Nízkoúrovňové fce vyhazují výjimky. Ok, na tom je shoda.

Vyšší celky, jako např. Content, Advert apod. výjimky vyhazovat nemusí - je to plně v kompetenci toho, kdo navrhuje architekturu aplikace. Klidně to mohou být objekty které v metodě ->asText() vrátí výsledek nebo nic a v metodě ->errorCode() oznámí případnou chybu. Anebo to budou fce které vrátí návratvou hodnotu, přesně jak píše Jakub.

Podstatné je, že pokud dojde k neočekávané události UVNITŘ metody, tak se vyhodí výjimka (uvnitř) a celek s tím počítá a vrátí validní výsledek (zvenku se ta výjimka nepozná). Pokud komponenta není schopná vrátit očekávaný výsledek (třeba v případě, kde 'nic' nebo 'false' není akceptovatelný výsledek), tak vyhodí výjimku a pak si s tím MUSÍ poradit nadřízený.

Otázkou tedy je, proč by měla getBest() vyhazovat výjimku při chybě DB, když je pro tebe prázdné pole validní výsledek. Diskutovat se pak dá o tom, jestli to je skutečně validní, ale to už je věc na jiné úrovni - věc zadání. (Zůstává problém, jak/jestli rozlišit skutečně prázdný výpis od prázdného výpisu při chybě.)

ikona Jakub Vrána OpenID:

Při použití výjimek je logické, aby getBest() vyvolala výjimku v případě, že v ní dojde k chybě. Vysvětloval jsem to tady: http://php.vrana.cz/osetrovani-chyb.php#d-10342. Jen ještě připomenu, že prázdné pole je korektní výsledek (na rozdíl od chyby v dotazu).

ikona David Grudl:

Ufff - tím "při použití výjimek" myslíš, že všechny funkce musí povinně vyhazovat výjimky i v případě, že je to nepraktické? To zní jako výjimkový masochismus ;)

ikona David Grudl:

Ha a teď tě ošklivě předběhnu a napíšu za tebe, že přece právě o tom ten článek byl! :-)

(právě že nebyl, v článku píšeš o těch nejvíce low-level funkcích. Byť by šlo o nešťastné vyjádření, tak prostě mezi "Vezměme si třeba takovou funkci mysql_query" a "Vezměme si třeba takovou funkci pro zvýšení počtu čtenosti" znamená klíčový rozdíl)

ikona Jakub Vrána OpenID:

Mějme dvě stránky – stránka pro zobrazení výběru článků a stránka s detailem článku, kde se jen tak mimochodem v bočním sloupci zobrazí seznam článků z tohoto výběru. Moje řešení je udělat jednu funkci getBest(), která v případě chyby vrátí false (a chybu zaloguje). Tato hodnota se na samostatné stránce ošetří (chybou 500), v bočním sloupci se s ní pracuje stejně, jako kdyby byl seznam prázdný (nic se nezobrazí). Implementaci této funkce jsem uváděl v této diskusi: http://php.vrana.cz/osetrovani-chyb.php#d-10347.

Jak bys v tomto konkrétním případě postupoval ty? Jednu implementaci funkce getBest() jsi už uvedl, ta používá výjimky: http://php.vrana.cz/osetrovani-chyb.php#d-10348.

ikona David Grudl:

Už mě to neba ;)

Šaman:

Reakce na: "Při použití výjimek je logické, aby getBest() vyvolala výjimku v případě, že v ní dojde k chybě."

Nemyslím si to. Funkce musí vyhodit výjimku, pokud skončila chybou. Interní výjimky si přece může ošetřit sama. A jestli to, že nemůže načíst požadovaná data není chyba hodná výjimky, tak ji nevyhazuj. Vždyď to je to, o co ti jde, ne?

Fce mysql_query() výjimku vyhodila, protože skončila chybou, která znemožňuje další (závislou) práci. Takže MUSÍ ošetření tohoto stavu delegovat výše.

Fce getAdvert() ji vyhazovat nemusí, pokud vrátí výsledek který další práci neovlivní. Tj, pokud si s výjimečným stavem poradí k tvé spokojenosti sama, tak NEMUSÍ delegovat problém vyšším instancím.

Pokud v některých případech chceš metodu označit za kritickou a někdy ne, tak řešení pomocí přepínače se mi jeví jako vyhovující. Nicméně pak bych v případě chyby vyhazoval zase výjimku. Proto, aby se všechny funkce chovaly stejně. Byl by v tom bordel, kdyby jedna fce vracela hodnotu, jiná házela výjimky.

ikona Jakub Vrána OpenID:

Tady máme definici funkce getBest(), která vyhazuje výjimku: http://php.vrana.cz/osetrovani-chyb.php#d-10348. Vypořádat se s ní sama nemůže, protože neví, v jakém kontextu bude použita. Předávat funkci parametr, který ovlivní, jestli se vyhodí výjimka nebo ne, mi přijde jako obstojný nesmysl, implementace této funkčnosti by navíc elegancí zrovna neoplývala.

Šaman:

<?php
function getBest($unnecessary = FALSE) {
  $result = '';
  try {
    $result = mysqli_query(..., "SELECT * FROM article WHERE best")->fetch_all(MYSQLI_ASSOC);
  } catch (Exception $e) {
    if (!$unnecessary) throw $e;
  }
  return $result;
}
?>

Je to více psaní a argument TRUE nebo implicitní FALSE není samopopisný, takže by se asi měla použít konstanta:
$best = getBest(UNNECESSARY);
Nicméně při použití fce je to už méně psaní, než s použitím návratové hodnoty a sama FUNKCE dělá to co potřebujeme. Pokud skonnčí chybou, háže výjimku. Pokud ji označíme jako postradatelnou, tak vrátí prázdný řetězec. Vrací tedy string nebo výjimku.
Na návratové hodnotě se mi nelíbí, že fce vrací string, nebo bool, nebo nedejbože výjimku (pokud není celé tělo fce v try-catch bloku, tak to může nastat) a výsledek tedy musím pokaždé ještě kontrolovat a podle něho se rozhodnout co dál.

ikona Jakub Vrána OpenID:

A takhle bys chtěl napsat všechny funkce pro získání dat do komponent (např. getBest(), getRecent(), getJobs(), …)? Takže místo elegantních jednořádkových funkcí budou monstra podle tohoto „návrhového vzoru“. To je tedy nápad! Proč se navíc v případě chyby vrací prázdný řetězec, když se jinak vrací pole?

Šaman:

Ajo, sory, to pole jsem si neuvědomil. Měla by vracet prázdný platný výsledek, takže pole.

Kolik takových funkcí tam máš? Desítky? Pak by se to dalo ještě rozvrstvit. Napsat si vlastní myQuery($sql, $unnecessary = FALSE), která bude implementovat tenhle 'vzor' a v konkrétní funkci jako getBest($unnecessary = FALSE) budeš řešit jen SQL dotaz a předávat případný parametr. Ve výsledku to bude kratší než obě možnosti které se řešily v diskuzi. (Jen jeden univerzální try-catch a nebude nutné kontrolovat návratovou hodnotu).

V tutej diskuzi už asi pokračovat nebudu, strhla se v takový malý flamewar jestli chyby ohlašovat vždy výjimkama, nebo se může i jinak. A holt máme rozdílné názory, to se stává.

ikona Jakub Vrána OpenID:

Kód rozhodně kratší nebude. Sjednocení chybové návratové hodnoty s prázdným výsledkem má zásadní nevýhodu v tom, že někdy můžu chtít tyto dva stavy odlišit.

Šaman:

Právě k tomu slouží přepínač. Pokud vím předem, že výsledek fce má jen doplňkový informativní charakter, tak potlačím vyhazování výjimek. Ve všech ostatních případech dostanu buď správný výsledek, nebo výjimku. A tu si mohu ošetřit jak budu chtít. Bloudíme v kruhu - ty teď namítneš, že test na návratovou hodnotu je kratší, než try-catch. :p

Takže to vezmu z druhé strany - proč já používám výjimky, i když je s tím více psaní?
1) Je to bezpečnějsí - viz Davidovo argumenty. Chyba se nedá tiše ignorovat, MUSÍM ji vyřešit.
2) Je to standardní OOP způsob, se kterým počítají i další programátoři, kteří přijdou s kódem do styku. U veřejných knihoven to beru jako podmínku použitelnosti, protože
3) Výjimka se při ladění krásně vystopuje. Naopak neočekávaná chyba která tiše nahlodává aplikaci je téměř nevystopovatelná.

Takže když TY, ve SVÉM kódu použiješ návratové hodnoty, tak asi víš proč to děláš a kdy si to můžeš dovolit. Ale v práci (děláme online objednávky) nebo ve sdíleném kódu bych si to netroufl.
Proto asi ta dlouhá diskuze - ty totiž radíš ostatním, ať (když si myslí, že to projde) výjimky nepoužívají. Respektive to tak vyznívá.

ikona Jakub Vrána OpenID:

1) Že se chyba indikovaná výjimkou nedá ignorovat, je tvůj zásadní omyl. V PHP se naopak dá ignorovat bez sebemenších problémů, čehož následkem je fatální chyba aplikace. A že se třeba kvůli nepovedené inkrementaci čítače nezobrazí stránka s detailem článku nebo produktu, může být pro webovou aplikaci životně problematické.

Už jsem to ostatně vysvětloval: http://php.vrana.cz/osetrovani-chyb.php#d-10390

3) Tichého nahlodávání aplikace ses dopustil pouze ty, konkrétně v http://php.vrana.cz/osetrovani-chyb.php#d-10408. Já radím v případě chyby tuto zalogovat (standardním mechanismem PHP), ty chybu bezostyšně potlačíš, takže se o ní nikdy nikdo nedozví.

Šaman:

:P Pokud je následkem něčeho fatální chyba aplikace, tak se to ignorovat nedá. Pokud všechno jakžtakž funguje, tak se to ignorovat dá, ale je to špatně.

Logování je samozřejmost. Neber můj kód doslova, ale jako nástin. Naopak, při logování v té myQuery(..) se loguje jen jednou, ale v tvém návrhu musíš mít kód pro logování po každém testu návratové hodnoty.

Ale už tu jenom flamujem. myQuery() se dá přepsat na návratovou hodnotu a přitom může chyby logovat. Takže minulý odstavec vlastně není argument související s výjimkama.

Jde tedy jen o to, jestli je lepší povinný transport vrtulníkem do nemocnice i se zadřenou třískou, nebo riskovat ignorování uříznuté nohy..
Jak je vidět, každý přístup má své zastánce.

ikona Jakub Vrána OpenID:

Kde bereš poznatek, že se fatální chyba aplikace nedá ignorovat? Co ti to nedovolí? Když k fatální chybě dojde třeba jednou za tisíc požadavků nebo za nějakých specifických okolností, které při testování nenastanou, tak aplikace taky jakžtakž funguje.

Chyby PHP se jen volbou v konfiguraci mohou logovat zcela automaticky (např. mysql_query() chybu vyvolá při zapnuté konfigurační direktivě mysql.trace_mode). Při tomto způsobu ošetření chyb v aplikaci žádné error_log() ani být nemusí. Když naopak chyby reportuješ pomocí výjimek a za určitých okolností je převedeš na pravdivostní hodnotu, tak se o logování musíš postarat sám.

Tvoje funkce getBest() je tedy jednak špatně navržena a jednak i špatně implementovaná. My se tady bavíme o ošetřování chyb a ty z tohoto pohledu nejdůležitější část funkce (zalogování chyby) vynecháš a pak prohlašuješ něco o nástinu.

A co se toho příměru týče – výstižnější by mi přišlo spíš „kvůli zadřené třísce pacienta raději utratit, aby se netrápil“. To totiž podstatu neošetřené výjimky v nějaké postradatelné komponentě vystihuje lépe.

ikona Jakub Vrána OpenID:

A ještě jeden postřeh. Co když bude getBest() potřebovat provést dvě operace, třeba položit dva dotazy? Pak nezbývá nic jiného, než použít onen „návrhový vzor“ pěkně uvnitř této funkce nebo rezignovat na vlastnost, ve které jsou výjimky nejsilnější (hromadné ošetření chyb v posloupnosti operací, které spolu souvisí) a každou další operaci zavolat až po kontrole návratové hodnoty té předchozí.

Šaman:

Pokud bude nějaká posloupnost uvnitř funkce (kterou budu pokládat za atomickou, tj. dále nedělitelnou), tak budou všechny dotazy uvnitř try-catch a funkce jako celek buď proběhne a vrátí výsledek, nebo neproběhne a vyhodí výjimku/vrátí prázdný výsledek.

Šaman:

Jo, už chápu co myslíš. Ano, bude potřeba použít podmíněné vyhození výjimky uvnitř každé funkce, která bude obsahovat víc než jednu query.

Ani jedno řešení není ideální, takže je jen na osobních preferencích, které kdo zvolí.

Samozřejmě to lze dělat tak jak píšeš ty, ale pro mě je moc velký risk mít funkce, které i když nevrátí to, co mají (ani správný typ), tak nevyhodí výjimku. Asi by se mi občas stalo, že při chybě databáze by se mi na stránky vypisovalo FALSE. Osobně preferuji radši vyhodit pětistovku. Tedy ano, celé to zaříznout.

ikona Jakub Vrána OpenID:

Mě se v první řadě nelíbí ten nápad, že by se funkce měly podle parametru rozhodovat o tom, jakým způsobem budou hlásit chyby.

Podobně jako jiní diskutující zastáváš názor, že pokud se nepodaří vypsat nějaká bezvýznamná komponenta, tak raději stránku vůbec nezobrazíme. Tomu tedy říkám minimalizace rizika…

kvakoš:

Je to už starší diskuse, ale překvapilo mne, jak málo lidí souhlasí s Jakubem. Z mojí zkušenosti klienti ocení, když se dopad chyby ve webové aplikaci minimalizuje. Ještě jsem nezažil klienta, který by ocenil, že je detailně informován o všech chybách i méně podstatných, které se v aplikaci vyskytly. Naopak, klient chce mít klid a chce, aby chyby buďto nebudou, nebo je programátor sám rychle odstraní.

Ideální je tedy veřejně chybu přiznat 500 když už to nejde "veřejně zatlouct" - chybí hlavní článek, nebo produkt apod... Všechny chyby ale logovat a log pravidelně sledovat a problémy řešit.

Je to přece logické a tak se divím, že tolik lidí s tímto pragmatickým a selským přístupem má problém.

Jakub navrhuje bezvýznamné chyby logovat a navrhnout kód tak, aby chyba v bezvýznamné komponentě nezpůsobila že se nevykreslí jiné komponenty, které by se vykreslit mohly.

Diskuse je zrušena z důvodu spamu.

avatar © 2005-2024 Jakub Vrána. Publikované texty můžete přetiskovat pouze se svolením autora. Ukázky kódu smíte používat s uvedením autora a URL tohoto webu bez dalších omezení Creative Commons. Můžeme si tykat. Skripty předpokládají nastavení: magic_quotes_gpc=Off, magic_quotes_runtime=Off, error_reporting=E_ALL & ~E_NOTICE a očekávají předchozí zavolání mysql_set_charset. Skripty by měly být funkční v PHP >= 4.3 a PHP >= 5.0.