Kde zachytáváte výjimky?

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

Představte si aplikaci podle návrhového vzoru MVC nebo podobného, napsanou třeba v Nette, ve které chceme někde v postranním menu zobrazit třeba výběr článků. Pokud se tento výběr nepodaří načíst, neměl by zamezit zobrazení hlavního obsahu stránky. Model může vypadat třeba takhle:

<?php
class Article {
    static function findBests() {
        return dibi::fetchAll("SELECT * FROM [article] WHERE [best]");
    }
}
?>

Prezenter:

<?php
class ArticlePresenter {
    function renderDetail($id) {
        // tady bude načtení hlavního obsahu stránky - detailu článku
        // kromě toho načteme i výběr článků pro postranní menu
        $this->template->bestArticles = Article::findBests();
    }
}
?>

Šablona postranního menu:

{if $bestArticles}
	<h3><a href="{link Article:bests}">Výběr článků</a></h3>
	<ul>
	{foreach $bestArticles as $article}
		<li><a href="{link Article:detail $article->id}">{$article->title}</a></li>
	{/foreach}
	</ul>
{/if}

Zdrojový kód je k dispozici na GitHubu.

Jak je vidět, model používá Dibi, které v případě chyby vyvolá DibiException. A mě zajímá, jak s touto výjimkou naložíte. Ošetříte ji už v modelu, v prezenteru, nebo ji snad necháte probublat až na nejvyšší úroveň? A pokud ji nezachytíte už v modelu, použijete dokumentační komentář @throws? A nebude divné, že se prezenter prostřednictvím typu výjimky dozví o implementaci modelu? Nebo ji třeba v modelu přeložíte na jinou výjimku? A jak vlastně bude vypadat blok catch?

Všimněte si také toho, že v šabloně je odkaz na Article:bests – to bude stránka, kde je hlavním obsahem právě seznam nejlepších článků a kde je tedy žádoucí, aby skončila pětistovkou, pokud se seznam nepodaří načíst. A aby vypsala informaci o prázdném seznamu, pokud žádné články jako výběr ještě označené nebudou. Přijde mi logické, aby tato stránka vevnitř volala stejnou metodu modelu Article::getBests.

Ptám se proto, že jsem stejné otázky v osobním rozhovoru položil hlavnímu vývojáři významného českého frameworku a jeho odpovědi se mi zrovna dvakrát nelíbily. A pokud vám možnost výskytu chyby při čtení dat z databáze přijde nepravděpodobná, tak si představte, že se ptáte nějaké nespolehlivé služby (třeba Twitteru). Pokud byste v tom případě své řešení upravili, tak si ještě odpovězte na otázku, jakou chyba musí mít pravděpodobnost, aby stálo za to s ní nezacházet jako s fatální.

Viz řešení.

Jakub Vrána, Řešení problému, 9.8.2010, diskuse: 110 (nové: 0)

Diskuse

Sniper:

Dibi vyjimku zachytit primo v modelu a prelozit na nejakou jinou, napr. ArticleException, kterou pak zpracovat v controlleru. @throws je samozrejmosti.

Pripadne jeste z modelu do controlleru vratit prazdnej datovej objekt, to by pak zalezelo na konkretnich pozadavcich.

ikona Jakub Vrána OpenID:

Díky za odpověď. Mohl bys prosím napsat i kód, ať se bavíme konkrétně?

Požadavky jsou jasně dané. Vrátíš tedy prázdný datový objekt nebo vyvoláš výjimku? A jak s prázdným datovým objektem naložíš v akci Article:bests? Tam je potřeba rozlišit dva stavy – došlo k chybě nebo zatím nic nebylo označeno.

Sniper:

Primo v Nette nevim, ale v Zendu by to bylo nejak takhle:

Model Db_Article
<?php
class Db_Article extends Zend_Db_Table_Abstract
{

    /**
     * Vraci seznam BEST clanku
     *
     * @return Zend_Db_Table_Rowset_Abstract|null
     */
    public function fetchBest()
    {
        try {
            return $this->fetchAll(array('best = ?' => 't'));
        } catch (Zend_Db_Table_Exception $ex) {
            App::log->log($ex->getMessage(), Zend_Log::ALERT);
            return null;
        }
}
?>

V controlleru se nic delat nemusi, jenom predam do view, kde bude:
<?php
if (!is_null($this->bestArticles)) {
    vypise_best_clanky(); // pseudo
}
?>

Ano, vim ze uzivatel nepozna jestli doslo k chybe nebo jestli nic nalezeno nebylo - coz je i ucel. Uzivateli to je jedno, ten proste best clanky ma nebo nema.

ikona Jakub Vrána OpenID:

Díky. Ano, takhle znělo zadání. Předpokládám, že metoda App::log() zprávu někam zapíše. Při vývoji by se ale spíš hodilo, aby se rovnou zobrazila. Dalo by se toho nějak dosáhnout?

A jak by vypadala obsluha samostatné stránky Article:best? Na té je žádoucí, aby skončila pětistovkou, pokud dojde k chybě.

Sniper:

App::Log by v tomhle pripade byla instance Zend_Log, kterej podle toho, jakej mu nastavim adapter logovaci zpravu zpracuje. V produkci standartne loguju do DB, na develu napr. do firebugu. Celej Zend_Log konfiguruju pres konfiguraci aplikace v ini souboru.

samotna stranka article:best by byla nejaka action toho controlleru, cili bych reagoval v ni:

<?php
class ArticleController extends Zend_Controller_Action
{

    public function bestAction()
    {
        $dbModel = new Db_Article();
        $this->view->bests = $dbModel->fetchBest(); // pokud nic nenajdu, klidne do view predam prazdne pole
        if (empty($this->view->bests)) {
            $this->_response->setCode(505);
            // pripadne nastavim nejaky hlasky
        }
    }

}
?>

ikona Jakub Vrána OpenID:

To je pěkné, díky za vysvětlení.

Ještě se mi moc nelíbí ten test na empty(). Pokud se vrátí prázdné pole, tak to přeci znamená korektní výsledek, že žádný výběr článků ještě není.

martin:

v danom priklade by som chybu odchytaval az v renderDetail metode.
(a pravdepodobne by som si na zobrazenie best clankov vytvoril samostatnu komponentu).
<?
class ArticlePresenter {
    function renderDetail($id) {
        // tady bude načtení hlavního obsahu stránky - detailu článku
        // kromě toho načteme i výběr článků pro postranní menu
        try {
            $this->template->bestArticles = Article::findBests();
        } catch (Exception $e) {
            //zaloguj chybu
            $this->template->bestArticles = false;
        }
    }
}
?>

a podla teba so zvysujucou sa pravdepodonost chyby je vacsia sanca, ze bude chyba fatalna? nie je to ak tak nahodou naopak? nevraviac o tom, ze tie dve veliciny spolu uplne nesuvisia podla mna.

ikona Jakub Vrána OpenID:

Díky za odpověď. Mohl bys prosím ještě napsat, jaký kód bys použil pro zalogování chyby?

Co kdyby těch volitelných komponent bylo třeba sedm? Pak by každá z nich měla kolem sebe tuto obálku?

Na pravděpodobnost chyby jsem se ptal proto, že řada lidí považuje třeba zrovna chybu při práci s databází za natolik nepravděpodobnou, že se s jejím ošetřením vůbec nenamáhá – tato chyba se tedy buď vůbec nezachytí (což způsobí fatální chybu PHP) nebo zachytí na nejvyšší úrovni (což se většinou přeloží na pětistovku, pokud to jde). Já žádnou souvislost mezi fatálností a pravděpodobností chyby nevidím.

martin:

keby tych komponent bolo sedem a chcel by som v kazdej uplne rovnake spravanie, tak kludne napisem try catch 7-krat. pripadne si urobim predka komponenty:

<?
abstract class UnnecessaryComponent {
    function renderDetail() {
        try {
            renderData();
        } catch (Exception $e) {
            //zaloguj chybu
        }
    }

    abstract function renderData();
}
?>
co sa tyka logovania, tak
<?
    Debug
::processException($e);
?>
by malo stacit.

co sa tyka kritickosti a pravdepodobnosti, asi som zle pochopil cast vety: "jakou chyba musí mít pravděpodobnost, aby stálo za to s ní nezacházet jako s fatální".

ikona Jakub Vrána OpenID:

Čeho přesně by bylo UnnecessaryComponent předkem? A co by bylo vevnitř renderData()?

Logování pomocí Debug::processException() má tu nevýhodu, že se při vývoji chová jinak, než v produkci, takže se chování při chybách těžko simuluje. V produkci se vždy zalogují a pokračuje se dál, při vývoji se buď potichu pokračuje nebo se přeplácne celá obrazovka (v závislosti na druhém parametru).

Co se pravděpodobnosti týče – někteří programátoři mají tendence málo pravděpodobné chyby zvlášť neošetřovat. Což při použití výjimek vede k fatální chybě aplikace.

martin:

ma byt predkem BestArticlesComponent,.... proste tych siedmich komponent, ktore ti budu zobrazovat nekriticke casti na stranke.

renderData komponenty BestArticlesComponent by bolo nieco ako:
<?
   
public function renderData() {
        $this->template->bestArticles = Article::findBests();
    }
?>

myslim, ze nie je problem nastavit Debug classu, aby sa aj na lokal spraval ako produkcne prostredie. nicmene s tymto nemam az tolko skusenosti, takze by som sa za to nebil.

aha, to je ale v pohode, nie? ked programator povazuje celu stranku za jednoliatu cast, tak ma failnut cela stranka. pokial ju nepovazuje, tak explicitne osetri vynimky v okoli jednotlivych komponent.

ikona Jakub Vrána OpenID:

Jak ty komponenty zapasuješ do současného prezenteru, případně do šablony? Rozumím tomu správně tak, že kvůli načtení dat pro jednu komponentu (nyní jeden řádek) budu definovat jednu třídu?

Jde o to, že rozhodnutí, zda bude chyba fatální pro celou stránku, často není stanoveno analyticky, ale z čiré pohodlnosti s vědomím toho, že k chybě doufejme nedojde.

martin:

componentu zapasujem nejak takto (vycuc z .phtml template-y):
<?php
   
{widget BestArticles}
?>
a ano, kludne definujem jednu triedu - ak by som chcel dany kus stranky pouzit aj niekde inde, mam velmi jednoduche znovupouzitie.

ano, nicmene aj ked je programator lenivy alebo v tomto pripade skor bastlic, tak sa na tu chybu pravdepodobne pride, ked nastane.

ikona Jakub Vrána OpenID:

Pořád mi toho tady dost chybí – nestačí přece vytvořit třídu a do šablony napsat {widget}. Komponenta se taky musí umět vykreslit, musí si načíst šablonu a tak dál. Mohl bys kód prosím dopsat do kompletní ukázky, abych si mohl udělat celkovou představu?

Na chybu se přijde, ale důsledky jsou nemilé: „Půl dne nám nepřišla žádná objednávka, protože se nepodařilo načíst nejprodávanější produkty.“

martin:

ano, samozrejme, treba k tej komponente vytvorit sablonu, ktora bude obsahovat presne to, co si napisal ako zdrojak pre sablonu.

pri ignoracii chyb ale moze byt uplne rovnaky problem: pol dna nam neprisla ziadna objednavka, lebo sa pre produkt sme mali nieco taketo:
<?
  $count
= getAvailableCount($product);
  if ($count > 0) {
    $allowBuying = true;
  } else {
    $allowBuying = false;
  }
?>
ziaden rozdiel. teda az na to, ze tato chyba urcite nebude reportovana ako kriticka. a preto sa na to dojde o dost neskor...

ikona Jakub Vrána OpenID:

Mohl bys prosím napsat celou tu třídu? Tedy včetně metody render() a případné registrace oné třídy.

S ignorací chyb máš samozřejmě pravdu. Já říkám jen to, že s chybou bych měl zacházet podle toho, jak je vážná, nikoliv podle toho, jak se mi zdá pravděpodobná.

martin:

ufff, no ze si to ty ;) aj ked ta textarea ako editor nie je nic moc :)
komponenta BestArticlesComponent.php :

<?
   
class BestArticlesComponent extends UnnucessaryComponent {
        public function renderData() {
            $this->template->bestArticles = Article::findBests();
        }
    }
?>

template BestArticlesComponent.phtml:
<?
{if $bestArticles}
    <h3><a href="{link Article:bests}">Výběr článků</a></h3>
    <ul>
    {foreach $bestArticles as $article}
        <li><a href="{link Article:detail $article->id}">{$article->title}</a></li>
    {/foreach}
    </ul>
{/if}
?>

a tot vsjo, uz ju len includnes do akejkolvek template-y pomocou:
<?
   
{widget BestArticles}
?>

tie kody template nie su phpckove, ale neviem, ako tu urobit nie-php kod.

co sa tyka druhej casti, tak vaznost chyby je pre mna dost abstraktny pojem. ta ista chyba totiz moze byt na roznych miestach inak "vazna". je dolezite priradovat chybe vaznost? resp. nie je dolezite je priradit vaznost vzhladom k pouzitiu, pripadne kontextu?

ikona Jakub Vrána OpenID:

Ale vždyť toho kódu není ani půlka. Chybí zdědění z Control, metoda by se měla jmenovat pouze render(), chybí nastavení souboru s šablonou, chybí její vyrenderování a chybí metoda createComponentBestArticles. Navíc by se ve {widget} mělo psát malé písmeno.

Co se vážnosti chyby týče, tak samozřejmě. Např. metoda Article::findBests() způsobí vážnou chybu, pokud ji voláme na stránce určené pro výpis výběru článků a méně vážnou chybu, pokud ji zavoláme pouze někde z bočního menu.

martin:

ja som myslel, ze ide o povedanie si principov a nie ucenie sa nette. alebo ti ide o ratanie kodu?
pokial jedno alebo druhe, povazuj moj kod za nezaujimavy.

ikona Jakub Vrána OpenID:

U tohoto článku mi šlo o konkrétní kód, proto jsem ostatně příklad zveřejnil i na GitHubu. Ono se totiž často řekne, to bych udělal takhle a takhle a přitom tím každý myslí něco jiného. Proto jsem chtěl co nejpřesnější kód, abych pak jednotlivá řešení mohl porovnat. O počítání řádek kódu mi nejde, spíš o elegantnost jednotlivých řešení. A když někdo napíše jen půlku kódu, tak to pak může vypadat elegantnější, než to ve skutečnosti je…

martin:

pre porovnanie uplne staci ta cast, ktoru som napisal v prvom prispevku, nie? dalsie casti boli len odpovedou na tvoju otazku, co by som robil, keby. takze neviem uplne, co chces s cim porovnavat.

Martin Hruška:

Už té série článků o chybách prosím nech. Už to přestává  být sranda :/

ikona v6ak:

Webové aplikace jsou trošku zvláštním případem ohledně chyb. U desktopu nebo u mobilu je potřeba uživateli srozumitelně popsat chybu, je-li očekávána (tedy ne nějaké InvalidArgumentException). To vlastně v obou případech. Ale u webové aplikace uživatel nebude očekávat chybu typu "problém s databází", stačí mu "SRY, nejede to, chyba je u nás.", zatímco u desktopu se mu hodí vědět třeba to, že má k nějakému souboru správně nastavit práva. Pokud se k nějaké \DibiException dostane programátor (má mnohdy až zbytečně podrobný stacktrace), ví co s tím.
V Javě jsem webové aplikace moc nedělal. Máme tam navíc řízené výjimky (ty, které musí být zachyceny nebo uvedeny v throws). Je běžná praxe výjimku jako \DibiException obalovat abstraktnější výjimkou.
Měl-li bych se bavit o vysněném řešení v Nette, zavedl bych nekritické komponenty a snad i anonymní šablony (abych nemusel mít šablony ve zbytečně moc souborech) a pokud by pak došlo k vyjímce třeba při vytváření komponenty, framework by tam automaticky hodil předem připravenou chybovou hlášku a chybu by předem daným způsobem reportoval (log, mail, ...).

martin:

co si predstavujes pod obalovanim abstraktnejsou vynimkou. nieco ako ArticlesException?

ikona v6ak:

Ano, přesně tak. Možná by šlo použít třeba IOException nebo DataAccessException, ale to je teď celkem jedno.
Ve webových aplikacíxh ale na to většinou celkem kašlu, protože výjimka pak bude stejně jen logována a způsobá nějakou chybovou hlášku. Něco jiného je třeba výjimka informující o duplicitě (může-li normálně nastat), tam je samozřejmě nutné to nějak interpretovat a to určitě v modelu, jinak mám nechutně a hlavně zbytečně prosakující abstrakci.

martin:

tak podla mna je ArticleException uplne zbytocna chyba ;) a pattern, kde sa v kazdej vrstve chyba akurat zabali do dalsej, v podstate nic nehovoriacej chyby, je hrozny.

ikona v6ak:

Ale má to svoje opodstatnění, já i Jakub jsme to psali. Ale IMHO ne ve webových aplikacích.

martin:

osobne si myslim, ze v akejkolvek aplikacii zmysel nema. a specialne v jave by som za takuto prax strielal. vedel by som si predstavit chyby typu TemporaryErrorException alebo podobne. ale ArticlesException, aky by bol jej vyznam, resp. co to podla teba tomu kusu kodu, ktory ju odchyti, povie? a to vysvetlenie opodstatnenia som nejak nenasiel :(

ikona v6ak:

Takovéto chyby se lépe zpracovávají, pokud je chceš nějak prezentovat uživateli.

Franta:

Klíčové sousloví: prosakující abstrakce. Netýká se to jen parametrů metod a návratových hodnot, ale i výjimek.

ikona v6ak:

Takže, rozhodl jsem se trošku se podívat na makra a implementovat něco podobného, byť jsem se držel oproti původní představě poněkud více při zemi. Berte to (ten BasePresenter) zatím spíše jako ukázku, nějaký narychlo zbastlený koncept.

Přidal jsem i pár skriptů pro simulaci chyby spojení, nastavil lazy spojení (aby se chyba projevila na požadovaném místě) a přidal nějaký balastní text před a za onu část, aby bylo vidět, že chyba v této části neohrozí vykreslení zbytku. Jen v debug módu je celá stránka při chybě překryta onou Nette BSOD.

http://github.com/v6ak/mvc-exception-demo

ikona Jakub Vrána OpenID:

Zajímavé řešení! Díky.

TheTom:

Jelikož můžeme kdykoli vyměnit DIBI za něco jiného a zbytek aplikace o tom nemusí vědět, nebylo by úplně košer pouštět DIBI výjimku z funkce - kdo pak má ty zaběhnuté výjimky hledat, že :)
Takže rozhodně hned odchytit a místo ní vrhnout nějakou vlastní (NoArticlesException nebo tak něco) a tu pak zpracovat v prezenteru - ideálně asi v tom případě místo článků (jak v menu, tak na extra stránce) vypsat nějaký ten pokec pro návštěvníka, pětistovka by ho mohla zbytečně vyděsit...
Na druhou stranu, pokud VÍM na 100%, že u DIBI zůstanu, mohl bych se vykašlat na vlastní výjimku a nechat do prezenteru probublat DibiException, nebude v tom skoro rozdíl, jen to nebude tak pěkné a čitelné.
Tak jako tak tam bude @throws, tak jako tak se nebude bublat až nahoru.
Prázdný datový objekt bych nechal jen a pouze pro okamžik, kdy nic není označeno.

ikona Jakub Vrána OpenID:

Díky za odpověď. Mohl bys prosím napsat i kód, abychom se bavili konkrétně? Tedy hlavně to zpracování v prezenteru a pokec v šabloně.

TheTom:

Zkusím to, aspoň přibližně :)

Prezenter:
<?php
try {
   $this->template->bestArticles = Article::findBests();
} catch (
NoArticleException $e ) {
   $this->template->bestArticlesError = true;
}
?>

A šablona něco ve smyslu:

IF $bestArticlesError
    došlo k chybě, zkuste to později
ELSE IF $bestArticles
    výpis
ELSE
   nebyly nalezeny žádné vhodné články

ikona Jakub Vrána OpenID:

Díky! Bude se chyba někde logovat? Pokud ano, tak kde a jak?

TheTom:

Logovat můžeme v obou catch blocích - asi bude lepší logovat hned při chytání DibiException přímo v modelu, nebudeme zbytečně prezenter zatěžovat logováním, to nemá s prezentováním moc společného... Navíc můžeme logovat přímo chybové výpisy DIBI.
Na druhou stranu logování v prezenteru by bylo nezávislé na implementaci, ale bylo by s tím mnohem víc práce (převod dibi výpisů do vlastního univerzálního formátu ve vlastní výjimce).

ikona Jakub Vrána OpenID:

Takže spíš tedy v modelu. Jak přesně toto logování bude tedy vypadat? Nejlepší by bylo, kdybys zase napsal konkrétní kód.

TheTom:

To už radši nechám na fantazii čtenářů :)
Každý loguje jinak a jiné věci, asi by to chtělo mít taky zkušenost s chybami, co můžou DibiException způsobit, abych věděl, co můžu čekat, případně jak akutní to bude (jestli mám spíš psát maily, abych to hned přišel opravit, nebo to jen nacpat někam do DB až na to bude čas)...

ikona Jakub Vrána OpenID:

Můžeš využít veškerý komfort třídy Debug z Nette.

ikona v6ak:

Cpát do DB informace o DibiException asi není nejlepší nápad :D

bzuK:

Model:
<?php
/** @returns Array se články nebo null v případě chyby */
class Article {
    static function findBests() {
        try {
            return dibi::query("SELECT * FROM [article] WHERE [best]")->fetchAll();
        } catch(Exception $e) {
            // Třeba nějaké zalogování
            return null;
        }
    }
}
?>

Prezenter: nechat jak je v článku

Šablona: nechat jak je v článku (pokud je to jen nějaký doplňkový info-boxík stranou, nemá smysl zatěžovat uživatele tím, že mi něco na serveru nefunguje)

ikona Jakub Vrána OpenID:

Díky. Mohl bys prosím ještě uvést, jak by vypadalo to zalogování? Nevím, proč to všichni přeskakují.

Jak by vypadal prezenter pro akci Article:bests? Ten by měl v případě chyby v databázi skončit pětistovkou (protože to je jeho hlavní činnost).

bzuK:

V Article:bests bych testoval výsledek Article::findBests() na  tři varianty:
- rovnost s null (===)
- neprázdné pole
- prázdné pole

ikona Jakub Vrána OpenID:

Nechtěl bys zase napsat kód? Kde přesně by se testovala identita s NULL a jak by se ošetřila?

ikona Džoukr:

Zdravím. Osobně se mi nelíbí žádná z nabízených možností, protože jsem toho názoru, že by se ze spodních vrstev (ať BLL či adaptérů) neměla výjimka na prezentační vrstvu vůbec dostat. Pouze taková, kterou vyhazuje prezentační vrstva (ale u PHP mě taková komponenta nenapadá). Upřednostňuji řešení, kdy se výjimka odchytává na BLL vrstvě s tím, že se vrací jednoduchý objekt informující o (ne)úspěchu akce a kolekce se plní přes parametr odkazem. PHP už dělám jen nárazově (toho času ASP.NET WebForms), tak snad bude můj příklad pochopitelný. Kdyby ne, omlouvám se ;)

<?php
//navratovy objekt
class Result
{
    public $isSuccesful;
    public $message;
    // popr. dalsi informace, ktere chceme prenest na prezentacni vrstvu
}

// BLL vrstva
class Foo
{
    static function findBests(&$bestArray)
    {
        $result = new Result();
        $result->isSuccessful = true;

        try
        {
            $bestArray = dibi::query("SELECT * FROM [article] WHERE [best]")->fetchAll(); // predpokladam, ze vraci pole
        }
        catch(Exception $e)
        {
            $result->isSuccessful = false;
            $result->message = $e->getMessage();

            // vetsinou zde tez loguju
        }

        return $result;
    }
}
?>

Snad je můj záměr čitelný, skalním PHPčkařům se omlouvám za případné zprzění. ;)

martin:

ked uz robis taketo podla mna krkolomnosti, na co ti je v result ta message? nepredpokladam totiz, ze je prezentacnej vrstve k niecomu dobra...
a co ked je findBests metoda pouzita nie len v prezetacnej vrstve - to budes v modeli pracovat s Result classou?

ikona Džoukr:

Ad 1) Jak jsem psal v definici Result, je to čistě na tobě, co si nahoru pošleš za informaci. Osobně mívám Message jako další objekt, který obsahuje víc informací (text, trace, atd...), které chceš na prezentační vrstvě ukázat v případě chyby. Tohle byl takový quickwin, abys mohl napsat "něco se podělalo" a vypsat chybovou hlášku z Exception. Koneckonců si můžeš poslat nahoru jako property objektu Result celou Exception, to je na tobě.

Ad 2) Ano, přesně tak. Nevidím v tom problém - naopak. :)

ikona Jakub Vrána OpenID:

Díky za řešení, záměr je zcela jasný. Co ale s tím návratovým objektem v prezenteru a ovlivní to nějak kód v šabloně? Tedy jak přesně bude vypadat prezenter (opět prosím o kód) a jakým způsobem se bude logovat?

ikona Džoukr:

Omlouvám se, Jakube, s MVC si zatím (ani v PHP, ani ASP.NET) moc nehraju, ale řekl bych, že použití by mohlo být následující (bacha, je to rychlořešení v případě, že v presenteru nepotřebujete dál pracovat s Result objektem - například nemáte v závislosti na úspěchu načtení bestArticles žádnou další akci.

<?php
class ArticlePresenter {
    function renderDetail($id) {
        // tady bude načtení hlavního obsahu stránky - detailu článku
        // kromě toho načteme i výběr článků pro postranní menu
        $this->template->bestArticles = array();
        $this->template->result = Article::findBests($this->template->bestArticles);

    }
}
?>

Template bych pak osobně řešil jako:

{if $result->isSuccessful}
    <h3><a href="{link Article:bests}">Výběr článků</a></h3>
    <ul>
    {foreach $bestArticles as $article}
        <li><a href="{link Article:detail $article->id}">{$article->title}</a></li>
    {/foreach}
    </ul>
{else}
    Neco se nepodarilo - chyba {$result->message}
{/if}

Logování provádím vždy v BLL (chcete-li modelu) po ošetření chyby v catch bloku. Tam je to čistě individuální a záleží na tom, zda chcete logovat do DB/filesystému/mailu, či zda na to používáte jiný nástroj.

ikona Jakub Vrána OpenID:

Díky za popis, až na název proměnné $result je to jasné (taková proměnná by asi byla potřeba pro každou komponentu).

Ideální reportování chyby si představuji tak, že se na vývojovém serveru rovnou zobrazí, na produkčním kamkoliv zapíše (a následně třeba pošle e-mailem).

ikona Džoukr:

Ano, používám tento způsob pro všechny metody BLL vrstvy nehledě na to, zda je často výpadková, nebo standardní. Co se týče reportování, osvědčilo se mi zapisovat chybu vždy a vždy ji posílat na prezentační vrstvu s tím, že až na ní se rozhodnu, zda zobrazím, či nezobrazím (z nějakého globálního příznaku o běhovém prostředí produkce/dev).

Za tu nepřesnost se omlouvám, přehlednější by určitě bylo místo result dát articlesBestResult, obzvláště pokud se tento princip používá při všech BLL/model voláních.

martin:

preco potom nechces tie chyby rovno odchytavat na prezetacnej vrstve, ked si ich tam aj tak posielas?

cooler:

taky si říkám, proč je tam posílá :-)

ikona Džoukr:

Jak jsem už několikrát psal. Nemusíš si poslat vůbec nic. Jde o to, kolik detailů o dole vzniklé chybě chceš prezentační vrstvě poslat. ;)

ikona Džoukr:

Ze dvou důvodů:

1) Čistě z principu návrhu vícevrstvé aplikace. Proč to rovnou vše nedělat na jedné vrstvě?
2) Jedna věc je výjimku odchytnout, druhé je poslat ji nahoru v čistě informačním objektu. Navíc jde o to, že v případě chyby chceme zpravidla vyvolat další akci, která na prezentační vrstvu nepatří: logování, odeslání emailu, atd... Kdybys to odchytával až na prezentační vrstvě, musel by sis tohle všechno dělat sám a hlavně bys to dělal Xkrát dokola. (použiješ tu samou metodu vícekrát) Takhle si prostě zavoláš metodu a zajímá tě z hlediska prezentační vrstvy jen to, jestli dopadlo ok nebo ne a co o sobě při chybě řekla.

martin:

1, co ma spolocne propagacia chyb s viacvstvou architekturou? a specialne v pripade, ked si tie exceptiony aj tak posielas?

2, v nette je presenter skor na urovni controlleru, to view je az template, ktora je zobrazuje vysledok. inac nechapem uplne, preco si myslis, ze by som handlovanie na urovni presenteru musel robit x-krat.

3, ako by si napisal metodu getBestOrRandom, ktora by vytiahla najlepsie clanky a ak ich nie je aspon desat, tak nejake nahodne do desiatky? staci schema, a predpokladam, ze tam pouzijes metody getBest?

ikona Džoukr:

Možná se úplně nechápeme, což může být i moje chyba. ;) Možná se vyjadřuju špatně.

1) To, že si nahoru posílám celou Exception, toho teď lituju, protože se toho držíš zuby nehty. Byla to jen moje lenost. :) Nahoru si klidně pošli pouze pár stringových informací (kde došlo k chybě a nějaké povídání) a Exception jako takovou dole zahoď. K tvé otáce - propagace chyb takový problém není, jako spíš jejich ošetření v závislosti na místě vzniku. Není to dogma, je to zcela jistě věc názoru, ale osobně jsem na prezentační vrstvě dost striktní k tomu, co by měla mít na starosti.

2) Nette nepoužívám, takže bych se nerad pouštěl na tenký led. ;) To xkrát použití jsem myslel vyloženě v případě, že chyby z business vrstvy ošetřuješ až na prezentační vrstvě. V případě, že bys použil nějakou business metodu na vícero místech, musel bys znovu opakovat ono ošetřování (tedy try-catch na prezentační vrstvě).

3) Velmi jednoduše (je to náčrt):

<?php
...

public static function
getBestOrRandom(&$bests)
{
    $result = new Result();
    $result->isSuccessful = true;

    try
    {
        $bests = array();
        $resBest = self::getBest($bests);

        if($resBest->isSuccessful)
        {
            if(count($bests)<10)
            {
                // dopln info z nahodnych
                $random = array();
                $resRand = self::getRandom($random);

                if($resRand->isSuccessful)
                {
                    //spoj $random a $bests
                }
                else
                {
                    // nepodarilo se doplnit
                    // opet zalezi na chapani metody, zda je <10 chyba
                    // pokud ano, pak:
                    $result->isSuccessful = false;
                    // libovolne doplnkove info dle definice Result
                }
            }
        }
        else
        {
            // zde zalezi pouze na tobe, zda je to chyba cele metody
            // nebo se pokusime doplnit tech 10 z nahodnych
        }

    }
    catch(Exception $ex)
    {
        $result->isSuccessful = false;
        // libovolne doplnkove info dle definice Result
    }

    return $result;
}
?>

Přiznávám bez mučení - při velmi složitém skládání se to může jevit jako hodně kódu, ale používáš-li ostatní metody stejného objektu (vracející Result), máš perfektní možnost rozhodnout, kdy a za jakých okolností je jaká chyba chybou celé metody a kdy se pokusit o načtení dat jinak (viz tvůj příklad s OR random)

martin:

uz len k tej trojke :)
nie je jednoduchsie napisat:
<?php
...

public static function
getBestOrRandom(&$bests)
{
   $best = self::getBest();
   $count = count($best);
   if($count < 10) {
      $random = getRandom(10 - $count);
      return merge($best, $random);
   } else {
     return $best;
   }
}
?>
?

a tu chybu odchytavat vyssie. su samozrejme pripady, kde chyby do vyssich vrstiev nechceme posielat - napr. pri remotenych volaniach alebo podobne. ale v ramci aplikacie tuto striktnost uplne nechapem. a ak by som to robil, tak na to mam special vrstvu ktora tie chyby odchyta a neprebalujem chyby v metodach modelu/business logiky. inac ten priklad, co som uviedol a ktory si tak krkolomne (aspon podla mna) spisal, bola len velmi primitivna ukazka skladania volani business logiky. pri trochu komplikovanejsich ten kod raddsej ani nechcem vidiet ;)

mimochodom, to tvoje naplnanie parametru, ktore si posielas ako pole, to je tiez slusna vychytavka ;) ako si v rozhrani vynucujes, aby ti tam niekto neposlal nemodifikovatelne pole? a ako to riesis, ked navrtova hodnota ma byt objekt - to nan mas nejaky holder?

ikona Džoukr:

Nikdy jsem nepsal, že je ten kód krátký v případě skládání. Ale nepřicházíš o možnost si sám definovat závažnost chyb. Pokud na to pečeš (viz tvůj příklad), celý to obal do try-catch a neřeš nic. ;) S tím, že by sis to měl odchytávat výše ale stejně nemůžu souhlasit, ale na tom se asi neshodneme ;)

Co se týče toho parametru, v C#, kde tohle používám, je ten parametr definován jakou "out" (http://msdn.microsoft.com/en-us/library/t3c….71%29.aspx) s tím, že je to vždy objekt (pole jsem použil jen pro tento PHP příklad), který je před použitím stejně znovu inicializován (jeho stará hodnota nás z logiky věci nezajímá, protože ho v BLL naplníme). V PHPkách používám též výhradně objekt jako parametr k naplnění - typicky kolekci.

martin:

ad "Nikdy jsem nepsal, že je ten kód krátký v případě skládání." - to netvrdis, ale ja tvrdim, ze to je hrozne dlhe a neprehladne ;)

ad "Ale nepřicházíš o možnost si sám definovat závažnost chyb. Pokud na to pečeš (viz tvůj příklad), celý to obal do try-catch a neřeš nic. ;) " - toto som nepochopil, mozes to rozviezt? kde si chces definovat zavaznost chyb? a ako mam ten tvoj getBest() zabalit do try-catch, ved vacsina vynimiek je odchytena vo vnutri, nie?

ikona Džoukr:

Už je to trochu two men show, takže bysme to měli nějak zakončit. ;) S tím try-catch jsem to myslel tak, že pokud by business vrstva nevracela Result (tedy nebyl by použit můj způsob) a tobě by bylo jedno, kde došlo k chybě (zda při hledání Best, nebo Random), můžeš to celé obalit a neřešit, v které části chyba vznikla. Jelikož tvůj příklad nevrací Result, ale rovnou pole, dalo by se takové řešení zřejmě použít. Nebo můžeš každé volání zabalit a tím v catch rozlišit onu důležitost případné chyby, ale blbé je, že už se z catche nemůžeš vrátit zpátky.

Myslím, že už to zbytečně rozpitváváme a původní smysl se ztrácí. Čili zpátky ke kořenu celého článku: Chyby bych osobně z BLL a Adapterové vrstvy odchytával výhradně na BLL vrstvě, či nejpozději na poslední vrstvě před prezentační, rozhodně ne však na na ní. A o tento spor myslím celou dobu jde.

Žena mě nahání od kompu, každopádně díky za velmi zajímavou a přínosnou diskuzi - vždycky jsem rád, když mám o čem přemýšlet. Měj se hezky.

Lemrouch:

Důvod proč odchytávat a logovat chybu okamžitě při jejím vzniku je ten, že v tu chvíli máte nejvíce informací, co se tam dělo, uvedu příklad:
Uživatel chce vyhledat článek s nadpisem "PHP" - to je prezentační vrstva, BLL vrstva vytáhne někde z configu connectinon string do DB a zavolá nějakou metodu na adapteru, ten může například sahat přímo do DB nebo může hledat z nějaké cache, nebo může podle konfigurace volat WS, prostě cokoliv vás napadne. Při této akci dojde k chybě a přávě tady víte, co se stalo. Víte, že jste volali DB, nebo že jste se dívali do cache, nebo že se volala WS a máte všechny connection stringy, přihlašovací informace a případně i celý dotaz, tak jak se volal do DB (např. SELECT.......). Tady je vhodné zalogovat, protože při monitoringu vám to dá nejvíce infomrací!
Pokud "jen" vyhodíte chybu nahoru, tak můžete přijít o spoustu infromací, všechny konfigurace, connection stringy, atd....¨
Právě na tomto místě ošetřím všechny věci ohledně chyby a uživateli mohu zaslat pouze to, že se operace nepovedla a ať kontaktuje administrátora.
Prezentační vrstva není od toho, aby zpracovávala chyby, které se vyskytly například při komunikací s DB a aby skládala nějaké logy a někam zapisovala.

cooler:

Na druhou stranu každá operace lehce může vyhazovat svou vlastní exception (Exception_Db_Cache, Exception_Db_Query atd.) a na vyšších vrstvách odchytávat tu exception, kterou chci.

Tyto exceptions mohou mít (a myslím, že by měly mít) společného předka, lze je tedy odchytit všechny v jednom catch.

ikona v6ak:

"Důvod proč odchytávat a logovat chybu okamžitě při jejím vzniku je ten, že v tu chvíli máte nejvíce informací, co se tam dělo,"
Dovolím si nesouhlasit, protože:
* Mám stacktrace.
* Některé podrobnosti mohu dát např. do message.

Michal Prynych:

Jsem toho názoru, že ve většině případů, by měl kód výjimku zachytávat, pokusit se s ní vyrovnat(například ještě jedním pokusem volání metody) a pouze pokud se z chyby nedokáže zotavit sám, pak by měl vyhodit výjimku výš, s tím, že by měla být o stupeň abstraktnější. Z toho tedy vyplývá, že pokud metoda nedokáže načíst seznam článků a nedokáže ho načíst ani jiným způsobem než defaultním, pak by měla vyhodit výjimku (a ta by neměla být databázová("twitterová"), ale o jeden stupeň abstraktnější). Osobně pak mám svoje výjimky, které dědí od stejného předka, který umí logovat do error_logu, do souboru, do emailu, do databáze nebo nikam. Potom mám potomky této výjimky, z nichž každý má definovanou kritičnost a tedy i styl logování(kritické se posílají emailem, méně kritické jdou do error_logu).
K příkladu tedy: Každý kousek by se měl pokusit výjimku vyřešit, a pouze pokud to nezvládne, pošle výjimku výš. Tedy jak v metodě getBest bude try - catch, tak v presenteru bude try - catch. Presenter by na základě svých znalostí měl rozhodnout, jestli je vyjímka kritická nebo ne (tedy jestli se s ní sám dokáže vypořádat) a podle toho zobrazí stránku bez seznamu článků, nebo vyhodí výjimku výš, což by mělo zobrazit chybu 500.

ikona Jakub Vrána OpenID:

Díky za přehledný popis rozumného řešení. Pokud bude oněch nekritických komponent v jedné akci např. 7, napíšu tedy celkem 14 bloků try–catch?

Michal Prynych:

Přesně tak, 14 bloků try-catch. Výhodou tohoto řešení, je velká znoupoužitelnost komponent. Navíc v moderním editoru není problém try-catch vygenerovat klávesovou zkratkou. A co se týče přehlednosti kódu, tak tu v případě chyb já osobně trošku upozaďuji.

ikona Džoukr:

S tím posláním výš souhlasím, ovšem až do té předposlední. Co když je nahoře už jen prezentační vrstva? Nemyslím, že by u často vypadávajících služeb bylo záhodno, aby shazovaly celou vrstvu. Navíc z logiky věci by se prezentační vrstva moc rozhodovat neměla, ale měla by pouze zobrazovat. Rozhodovat o logice by měla pouze v rámci vlastní vrstvy (obsluha UI a případných chyb z ní plynoucích).

cooler:

Jo ty exceptions, to je téma.

Udělalo by mi radost, kdyby z toho vznikl i článek s nějakým závěrem :-)

Sám nepoužívám dibi. Exception ve svých aplikacích odchytávám na úrovni ArticlePresenter (jak navrhuje martin), kdy nejsem vázán na DIBI.

Ovšem je třeba DIBI exceptions převést na své vlastní exceptions - např. v override metody query(). Také se mohu svobodně rozhodnout, zda Exception odchytím (a případně zaloguju, což může být metoda log() samotné exception) nebo ji nechám probublat - a uživatel pak dostane tu zmíněnou hlášku "posralo se to u nás" a zároveň ji automaticky zaloguju.

Ještě by mě zajímalo takhle s tou DB jaké všechny typy exceptions kdo považuje za relevantní, tedy do jaké granularity se pouštíte. Jestli vám stačí Exception_Database anebo máte Exception_Database_Query, Exception_Database_Select, Exception_Database_Update ....

ikona Jakub Vrána OpenID:

Já jsem před více než měsícem napsal závěr a teď k němu postupně sepisuji argumenty…

cooler:

teď vážně nechci aby to vyznělo špatně - já ten minulý článek četl rychle - ale pochopil jsem, že je dobré je odchytávat na správném místě a teď že hledáme to vhodné místo v MVC :-)

ikona Jakub Vrána OpenID:

Zatím nebudu prozrazovat pointu tohoto článku. Zopakuji snad jen to, že podle mě výjimky nejsou za všech okolností ideální mechanismus pro zpracování chyb.

Peca:

Já to většinou používám stylem, že dotaz přes dibi (lze to aplikovat obecně) v modelu obalím do try - catch, přičemž v catch volám vlastní logovací metodu (jednoduchá statická třída nezávislá na frameworku) a pak zároveň volám return FALSE. V případě, že metoda projde v pořádku, vrací výsledek.
Obecně tedy metody v modelu vrací při chybě FALSE, při úspěšném vykonání výsledek. Většinou to takto stačí, nějaké hlášky pro uživatele pak řeší presenter, pokud je potřeba.
Občas je nutné nějaké detailnější rozpoznání, pak má modelová třída buď metodu nebo vlastnost, ze které se to dá zjistit.

ikona Jakub Vrána OpenID:

Díky za náčrt řešení. Nebylo by rozumné to nějak zobecnit? Tedy místo toho, aby každá metoda modelu měla vlastní try–catch a vždy to stejné logování a návratovou hodnotu, aby volaly metodu, která to udělá centrálně?

Peca:

No, nad zobecněním jsem nikdy neuvažoval, ono by se to nemuselo vyplatit. Záleží na složitosti modelu, někdy v tom try - catch bloku mám další věci, případě v odchycení něco dalšího nebo např. v některých případech logování probíhá do odlišných souborů. Asi by pro primitivní metody modelu s jednoduchými dotazy šlo zobecnění udělat, ale přijde mi zbytečné zas volat další metodu kvůli jednomu try-catch bloku.

Jakub:

A kdyby ta knihovna, kterou používáš, vyjímky nevyhazovala, tak ji jako nebudeš kontrolovat vůbec?

ikona Jakub Vrána OpenID:

Pokud by existovala metoda, která za normálních okolností vrátí pole, při nenalezení dat vrátí prázdné pole a v případě chyby vygeneruje varování a vrátí false, tak bychom s tvorbou komponenty do postranního menu byli hotovi – o případné chybě bychom se dozvěděli standardním mechanismem PHP, uživatele bychom s ní neobtěžovali, zadání bychom do puntíku splnili.

Na samostatné stránce bychom při vrácení false vygenerovali pětistovku.

eee:

Oproti vyjimkam vidim i par nevyhod:

- musime si stanovit navratovou hodnotu, ktera indikuje chybu. Pripadne nejaky flag.
- techto hodnot (ruznych chyb) muze byt vice, mohou v case pribyvat, coz muze zpusobit nutnost zmeny rozhrani funkci. U vyjimek menit rozhrani nemusite nikdy.
- pokud pouzivate specialni hodnoty (jako uvedeny false) misto pojmenovanych flagu, pak se musite bud spolehnout na nejakou konvenci nebo zavadet konstanty pojmenovavajici vznikle specialni situace.

ikona Jakub Vrána OpenID:

Za součást rozhraní považuji i vyhazované výjimky. Proto se ostatně píšou do /** @throws */.

ikona Ondřej Mirtes:

Nevím, jestli jen nezopakuji již vyřčený návrh, těmi 60 příspěvky jsem se prokousal jen letmo.

Rozvrhnutí zodpovědnosti je z hlediska MVC jasné - model nezajímá, kdo ho bude volat a jestli to pro toho zájemce budou kritická data nebo ne. Model by tedy v případě neúspěchu měl vyhodit výjimku, a jestli nechá probublat DibiException anebo ji zkonvertuje na abstraktnější BestArticlesException je na rozhodnutí programátora.

<?php

class ArticleModel {

  public function getBestArticles()
  {
    try {
      return dibi::fetchAll('...');
    } catch (DibiException $e) {
      throw new BestArticlesException('Couldn't retrieve best articles.);
    }
  }

}
?>

Presenter je to místo, kde bych se měl rozhodnout, jak moc kritická a potřebná ta data jsou. Podle tvého zadání teda příliš potřebná nejsou a aplikace by to měla ustát. Tu konkrétní výjimku, kterou vyhazuje model, bych v presenteru odchytil a udělal vše potřebné, aby se vykreslovaná šablona nemusela starat o to, že někde v presenteru počítám s možností, že nejlepší články se nepodařilo načíst.

<?php

class ArticlePresenter extends BasePresenter
{

  public function renderDetail($id)
  {
    $this->template->article = ArticleModel::getArticle($id);

    try {
      $this->template->bestArticles = ArticleModel::getBestArticles();
    } catch (BestArticlesException $e) {
      $this->template->bestArticles = array();
    }

  }

}
?>

Šablona už jen kontroluje počet článků v poli. Pokud je prázdné, můžu skrýt celý blok a nemusí se tak uživateli zobrazovat trapná hláška "nejlepší články nenalezeny".

<?php

{if count($bestArticles) > 0} //šlo by použít i empty, které ovšem nefunguje se složitějšími kolekcemi
  <h3>Nejlepší články</h3>
  <ul>
    {foreach $bestArticles as $article}
      <li><a href="{link detail $article->id}">{$article->title}</a></li>
    {/foreach}
  </ul>
{/if}
?>

Ještě k různé závažnosti výjimek - v rámci KISS (http://jdem.cz/gx7d9) bych nedělal věci složitějšími, než je třeba a výjimky rozdělil na ty, se kterými počítám (BestArticlesException) a se kterými nepočítám (obvykle low-level výjimka, která probublá všemi vrstvami a značí nějaký velký průšvih). Takové výjimky bych logoval nějakým jednotným mechanismem (Nette\Debug) a uživateli zobrazil pětistovku.

Např. v Doctrine 2 si mohu v DQL query zažádat o jeden výsledek - getSingleResult(). Korektní a nikterak zvláštní situace je ta, že ten jeden výsledek, o který jsem si zažádal, neexistuje a tudíž dostanu NoResultException (někdo do URL může zadat neexistující ID). S touto situací bych měl v každém případě počítat, NoResultException zachytit a nějak definovat tento stav.

Může se ovšem stát, že se nepodaří připojit k databázi, ptám se na neexistující sloupec v tabulce apod. V tomto případě mi probublá PDOException až na frontend a protože je to kritická chyba, která by neměla nastat, zobrazím pětistovku a chybu si nechám odeslat na mail.

Kdybych chtěl zamlčet a pokračovat ve zpracování requestu i všech chybách, které mohou nastat, můžu vzít kanón na vrabce a chytat v presenteru obecnou Exception. Osobně bych to nedělal, protože už pak není žádný způsob, jak se o chybě dozvědět (obvykle její vážnost vyžaduje můj zásah), ale i tato možnost tu existuje.

martin:

dotaz len k jednej veci z tvojho prispevku, ktora ma zaujima:
preco v presenteri odchytavas len BestArticlesException a nie akukolvek Exception - pri inej to ma spadnut cele a len pri BestArticlesException nezobrazit ten kus?  preco?

ikona Ondřej Mirtes:

I to by bylo možné a tuhle variantu zmiňuji v posledním odstavci v podobné situaci. I když mi to nepřijde jako nejlepší nápad.

martin:

prepac, nejak som to nedocital :)

ikona Jakub Vrána OpenID:

Prakticky stejné řešení tady už bylo: http://php.vrana.cz/kde-zachytavate-vyjimky.php#d-10496

Zcela nepřijatelné se mi u tvého řešení zdá to, že programátor se o chybě nikdy nedozví. Takže na stránkách se třeba půl roku nezobrazuje nějaká komponenta, ale chybový log zůstane prázdný.

Ještě se zeptám – pokud bude oněch nekritických komponent v jedné akci např. 7, napíšu tedy celkem 14 bloků try–catch?

manakmichal:

No, pokud by se použilo zalogování pomocí Debug::processException(), tak by se chyba uložila a poslala na email (závisí na parametrech) (kód Ondřeje Mirtese)

<?php

class ArticleModel {

  public function getBestArticles()
  {
    try {
      return dibi::fetchAll('...');
    } catch (DibiException $e) {
      Debug::processException($e)
      throw new BestArticlesException('Couldn't retrieve best articles., 500, $e); // volitelne parametry pro PHP 5.3
    }
  }
}
?>

<?php

class ArticlePresenter extends BasePresenter
{

  public function renderDetail($id)
  {
    $this->template->article = ArticleModel::getArticle($id);

    try {
      $this->template->bestArticles = ArticleModel::getBestArticles();
    } catch (BestArticlesException $e) {
      $this->template->bestArticles = array();
    }
  }
}
?>

takhle by se programátor o chybě dozvěděl (pokud by si nastavil Debug a nenačtená věc by se na stránce nezobrazila. Zobrazování pomocí komponent může být však elegantnější řešení, ale mně se i toto řešení líbí a je funkční

cooler:

Nahoře jsem se ještě zmínil o společném předkovi různých exceptions. Jasně, že to všichni víme, jen jsem chtěl zmínit, že pro pohodlnost práce s výjimkami je to velice důležité a přišlo mi, že by to tu mělo zaznít.

Např. Exception_Db_Cache a Exception_Db_Query by mohly mít např. společného předka Exception_Db. Odchytím si a ošetřím tedy jen ty výjimky, které si přeju, anebo třeba všechny zároveň.

Přijde mi, že s tímto se tady v těch debatách příliš nepracuje a že je to součást řešení autorovy otázky, ale může to být jen mylný dojem :-)

HosipLan:

Jsem pro vyhazovat vyjímky v modelu a buďto je všeobecně odchytit pomocí try{}catch(\Exception $e){} v presenteru a předat $e->getMessage() jako flash zprávu, nebo podle specifických případů třeba přesměrovat.

HosipLan:

ad:

obecně souhlasím s martinem
http://php.vrana.cz/kde-zachytavate-vyjim…=10545#d-10472

ale líbí se mi řešení džoukra
http://php.vrana.cz/kde-zachytavate-vyjim…=10545#d-10488

budu nad tím meditovat :)

ikona retro:

Osobně si myslim, že stačí pro veřejný služby udělat hezkou stránku pro [500] chybu. Ideálně s nějákym zvířátkem a omluvou. Obsah, pokud se kterákoliv část stránky nepovede zobrazit, vůbec neukazovat. Ještě obalit vyjímku nějákym skriptem, co vám pošle stack trace do emailu, aby jsme o ní věděli.

ikona Jakub Vrána OpenID:

Upozorním jen na to, že toto řešení odporuje zadání formulovanému v prvním odstavci. A vsadím se, že kdyby zadání chodívala do takovéto hloubky, bylo by to s nimi v rozporu prakticky vždy. Protože málokterý zadavatel na otázku typu „Má se znemožnit vytváření objednávek, když se nepodaří načíst seznam nejčastěji kupovaných produktů?“ odpoví ano.

ikona retro:

Já jenom nechápu pořád o co vám s těma vyjímkama jde. Pokud si napíšu na aplikaci pořádně testy, tak bych měl zjistit kde má aplikace problém a kde se může něco pokazit. Potom už se může stát snad jen to, že spadne třeba DB server nebo něco zvenku (třeba nějáká služba obecně) nefunguje. V tom případě je lepší nezobrazovat vůbec nic jen omluvu návštěníkovi.

Vyjímky mají svůj význam, ale tohle podle mne uplně není on.

ikona Jakub Vrána OpenID:

Neshodneme se v tom, že pokud jedna služba nefunguje, tak je lepší nezobrazovat vůbec nic. Hlavně o tomhle ta série článků o chybách pojednává.

ikona retro:

Však pokud se kvůli chybě nemůže zobrazit část stránky, jak si můžeme být jisti, že zbytek stránky se zobrazí správně (aniž by třeba vyhodil vyjímku) ?

Nahlížím na to prostě jako "všechno nebo nic".

Ještě pokud mluvíte o tom příkladu s objednávkami. Asi vyjde nastejno zobrazit nějaký text že se nepodařil načíst seznam nejčastěji kupovaných produktů (váš návrh) nebo zobrazit omluvu a stránku nezobrazovat vůbec (můj návrh). Ani jedno řešení návštěvníka nepotěší a na důvěře k webu nepřidá.

Můj přístup: Vyjímky určitě zachytávat, ale většinu z nich nepředkládat koncovým uživatelům (ať už ve srozumitelném textu).

ikona Jakub Vrána OpenID:

Správným zobrazením zbytku stránky si být jisti samozřejmě nemůžeme, to ale nikde nenaznačuji. Ty naopak naznačuješ, že si můžeme být jisti špatným zobrazením, s čímž nesouhlasím.

U webových stránek je většinou jedna nebo několik málo kritických částí, bez kterých nemá smysl zobrazovat nic. Třeba text článku na blogu, detail produktu v obchodě nebo třeba zpracování objednávky. Když se tohle nepovede, nemá smysl zobrazovat skoro nic dalšího (diskusi k neexistujícímu článku, fotky nenalezeného produktu nebo informace o možnosti dárkového zabalení neprovedené objednávky). Kromě toho je na stránkách ale typicky taky spousta komponent, kvůli kterým návštěvník na stránky nepřišel. Když chyba v těchto komponentách způsobí nezobrazení celé stránky, tak mám zásadní problém.

Příklad s objednávkami nastejno rozhodně nevyjde. Když člověk přijde na detail produktu třeba z vyhledávače a nezobrazí se mu nic, tak mám zásadní problém. Když se mu základní informace zobrazí a chybí pouze seznam nejčastěji kupovaného zboží, tak mu to může být úplně jedno, protože taková komponenta by na stránce vůbec být nemusela. Pravidelný návštěvník může být zmaten, pořád ale může s aplikací pracovat.

Zkrátka nejde o to výjimky jen zachytávat, stejně důležité je i to, kde se zachytávají. Pokud až někde centrálně na globální úrovni, tak je aplikace zbytečně křehká.

ikona retro:

OK, souhlasím. Každý na to pohlížíme jinak. Váš pohled hlavu i patu má. Díky za výměnu pohledů na věc.

ikona retro:

Ještě si dovolím reagovat na tu nespolehlivou službu uvedenou v článku.

Pokud to jde, informace z takové služby načítat asynchroně, až po načtení celé stránky z našeho serveru. Někdy to ovšem nelze, uznávám.

Jiří Knesl:

Je celá skupina vyjímek, které můžu mít:

1) MySQL gone away, uživatel nemá právo na tabulku apod. (zobrazím 500, chyba v best bude nejspíš jen projevem něčeho mnohem důležitějšího) - nechávám vybublat do ErrorPresenteru
2) query error (zobrazím 500, jdu natrhnout vývojářovi, který to SQL psal, zadel) - nechávám vybublat do ErrorPresenteru
3) prázdný result (vše je v pořádku, vracím nějaký objekt implementující Iterator, který je každopádně prázdný)
4) nějaká chyba v runtime (třebas alokace paměti) - 500, když to neselže teď, tak to selže o pár instrukcí dál, buď jdu do ErrorPresenteru, při různých chybách se může stát, že mi to Apache usekne a zobrazí jen bílou stránku
5) HYPOTETICKY: mám-li v User-Story, že nekritická chyba (tedy mimo 1, 4) nemá vést k přerušení zobrazování zbytku stránky (a mám-li best jen třeba v postranním panelu, ne hlavní obsah) - vyjímku odchytím v modelu (což je jen případ č.2, ten by se ale na ostrý server neměl nikdy (!) dostat)

Jak jsem ukázal, většina problémů by měla vést k 500 (protože jsou tak závažné, že shodí celou aplikaci tak jako tak a že to padlo právě na best je náhoda, popřípadě jsou to vadné query - a to už musí být, aby někdo nahrál na server verzi, kde ani nezkontroluje syntax sqlek, které používá)

ikona Jakub Vrána OpenID:

Zohledni prosím ještě případ, kdy k chybě dojde právě jen v tom jednom dotazu, přestože v něm není syntaktická chyba (třeba proto, že je poškozena MySQL tabulka, se kterou ten dotaz pracuje). Taky zkus nastínit, jestli a jak tyto chyby budeš rozlišovat.

5) Když výjimku odchytíš v modelu, jak pak bude vypadat obsluha akce Article:bests?

Nejlépe zkus napsat konkrétní kód – méně teoretizování, více praxe.

Jiří Knesl:

Máš to na Githubu (http://github.com/jiriknesl/mvc-exception-demo/) - těch .DS_Store si nevšímej :)

ikona Jakub Vrána OpenID:

Díky, konečně nějaký kompletní kód! Mám tyto otázky:

1. Jak bude vypadat obsluha stránky Article:bests, zmíněná v článku? Na té by se v případě chyby měla zobrazit pětistovka, protože jde o hlavní obsah stránky.

2. Nevadí ti, že se prezenter prostřednictvím vyhozené výjimky dozví o implementaci modelu?

3. Neměly by být tyto výjimky uvedené v dokumentačním komentáři metody?

4. Ten try–catch uvnitř Article::findBests() by byl součástí každé podobné metody modelu? Nebo bys to někam vyčlenil?

Jiří Knesl:

1. Jak bude vypadat obsluha stránky Article:bests, zmíněná v článku? Na té by se v případě chyby měla zobrazit pětistovka, protože jde o hlavní obsah stránky.

Pokud by mělo dojít k 500 na hlavní stránce Best a ne v postranním panelu, přesunul bych chytání vyjímky do odpovídajícího controlu. Tam nevadí, že control zná strukturu modelu, protože by v systémech s kvalitním OOP (jako je třeba PHP 5.3) ve stejném balíčku/namespace) a nevzniká poptávka po decouplingu.

2. Nevadí ti, že se prezenter prostřednictvím vyhozené výjimky dozví o implementaci modelu?

Presenter nebude nic vědět. Jen ví, že přišla "nějaká" vyjímka, ta by si spokojeně probublala do ErrorPresenteru (který by jen zkontroloval, zda nedostal vyjímku, která má způsobit jiný stavový kód jako Forbidden, Not Found...)

3. Neměly by být tyto výjimky uvedené v dokumentačním komentáři metody?

Spíš než metody si umím představit nějakou formu chytrého zpracování vyjímek (notifikace o vyhození vyjímek, nějakou obdobu AOP na throw, to hodně záleží na tom, co a jak potřebuju zpracovávat). Nejsem si ale úplně jistý, že jsem to správně pochopil, nechceš mi tu otázku víc vysvětlit?

4. Ten try–catch uvnitř Article::findBests() by byl součástí každé podobné metody modelu? Nebo bys to někam vyčlenil?

Jak jsem ukázal v předchozím komentáři, na server by se neměly dostat chyby, kterým lze zabránit už otestováním, takže by byl try catch blok jen tam, kde to jednoznačně vyplývá z výslovného přání klienta (přepokládám tedy korektnost tam, kde není výslovně čekaná robustnost).

optik:

Ano, takhle nějak se to běžně dělá a nevztahuje se to jen na nette, ale v podstatě na jakýkoli OO web framework. Mimochodem to není žádná novinka ale stará vesta, kterou např. Javisté, Rubisté... používají určitě léta. Jinak si zajdi na Jirkovo školení o testování, já sem tam sice nebyl, ale stačila mi přednáška na té letošní zdrojákové konferenci. Základ je psát aplikace objektově s testy nejlépe TDD a  zapomenout na $_GET, $_SERVER a podobné globální statické hrůzy a pak to krásně zapadne do sebe, někdy to zkus. Bez používání výjimek je OO jako bez nohy.

Jinak k 3. Neměly by být tyto výjimky uvedené v dokumentačním komentáři metody?

Je asi myšlen phpdoc komentář @throw(s). Je v podstatě zbytečné to psát, php na to nehledí (na rozdíl od C++, Javy). Je to čistě jen dokumentační záležitost. Z vlastní zkušenosti vím, že to ignorují i kolegové :). Možná to trochu vylepšují IDE, ale vimařům :) to nepomůže.

optik:

tohle byla sice reakce na Jirkův komentář, ale cílí na Jakuba.

ikona v6ak:

"php na to nehledí (na rozdíl od C++, Javy)"
C++ na to hledí? Kde?

optik:

V C++ může být seznam výjimek deklarován přímo v zápisu funkce

funkce() throw(výjimka,výjimka,...)
{}

pak se má hlídat vyhození nedef. výjimky, pokud je seznam výjimek prázdný, nesmí funkce vyhodit žádnou výjimku. Když tam throw() není, může být vyhozena jakákoli výjimka. Je to ale celé takové divné, moc se to nepoužívá a kompilátory to také asi moc nepodporují, ale je to tam.

ikona v6ak:

Tak děkuji za informaci. To jsem fakt netušil.

Franta:

O něčem podobném jsem psal tady: https://sql-vyuka.cz/d/node/29 – jde vlastně o hledání vhodného poměru mezi robustností a bezchybností. A většinou to je ve prospěch té robustnosti – nefunkčnost jedné komponenty by neměla postavit celý systém mimo provoz.

ikona Jakub Vrána OpenID:

Pěkné. Naše myšlenky se ubírají podobným směrem.

Pavol:

Model:
<?php
class Article
{
    /**
     * @return DibiRow
     * @throws Exception
     */
    static function findBests()
    {
        try {
            return dibi::fetchAll("SELECT * FROM [article] WHERE [best]");

        } catch (DibiException $e) {
            Debug::processException($e);
            throw new Exception('An error occurred', 0, $e);
        }
    }

    /**
     * @param int
     * @return DibiRow
     * @throws Exception
     */
    static function find($id)
    {
        // ... rovnaké ako findBests()
    }
}
?>

Presenter:
<?php
class ArticlePresenter
{
    /**
     * @param int
     * @return void
     */
    function renderDetail($id)
    {
        // kritická sekcia
        try {
            $this->template->article = Article::find($id);

            // nekritická sekcia
            try {
                $this->template->bestArticles = Article::findBests();
            } catch (Exception $e) {
                $this->template->robots = 'noindex,follow';
                // ...
            }

        } catch (Exception $e) {
            throw new BadRequestException('Article cannot be read.', 500, $e);
        }
    }
}
?>

View:
<h1>{$article->title}</h1>
<div>{$article->text}</div>

{ifset $bestArticles}
    <h3><a href="{link Article:bests}">Výber článkov</a></h3>
    <ul>
    {foreach $bestArticles as $article}
        <li><a href="{link Article:detail $article->id}">{$article->title}</a></li>
    {/foreach}
    </ul>
{/if}

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.