Pokud se rozhodnete tvořit aplikace podle architektury Model-View-Controller, dříve nebo později zjistíte, že postupy, které tak krásně vypadají na papíře, při praktické realizaci pokulhávají. Narazit se dá už u nejtriviálnějších úkolů – třeba obyčejný výpis článků na blogu.

Teorie hovoří takto:

  • model má metodu getArticles(), která vrátí všechny články (například z databáze jako result-set)
  • controller / presenter tuto metodu zavolá a získaný seznam předá do view
  • view se postará o vykreslení

V praxi se ale ukáže, že budeme chtít navíc:

  1. výpis stránkovat
  2. mít možnost řadit i podle jiných sloupců
  3. omezit výběr podle vlastní podmínky
  4. určit, které sloupce přenášet

Přitom půjde vždy o požadavky prezentační logiky. Model nemusí zajímat, že výpis článků stránkujeme (bod 1). Nemusí ho zajímat, že jsme se rozhodli na web přidat boxík s deseti nejčtenějšími články (body 2, 3, 4). Nemusí ho nic z toho zajímat, avšak postup, kdy získáme z databáze tabulku všech článků, ty setřídíme a vyfiltrujeme samotnou aplikací a nakonec zahodíme nepotřebná data, by v praxi představoval šílený overhead. Proto se zdá vhodnější rezignovat na MVC a zamořit model spoustou metod alá getArticles(), které vracejí články různě seřazené a filtrované, vždy na míru konkrétním požadavkům view.

…a nebo lze použít dibi 🙂

Řešení podle DibiDataSource

Dibi nabízí více způsobů, jak MVC paradox řešit, nicméně DibiDataSource je pro tento typ úkolu vyloženě navržen. Zkusím tedy nastínit koncept řešení.

Nejprve implementujeme model a jeho metodu getArticles(). Ta se pomocí SQL dotazu (libovolně složitého) dotáže na všechny články a jejich atributy.

class Model
{
	function getArticles()
	{
		return dibi::dataSource('SELECT ... FROM table1 INNER JOIN table 2 ... WHERE ...');
	}
}

Pokud dibi znáte, všimněte si, že místo obvyklého dibi::query() nebo dibi::fetchAll() zde volám dibi::dataSource(). Jinak se nic neliší.

Controller/presenter aplikuje stránkovací logiku:

class AnyPresenter extends Presenter
{
	...

	public function renderDefault($page)
	{
		$articles = $this->model->getArticles(...);

		// přidáme nějakou dodatečnou podmínku
		$articles->where('category=%i', $this->category);

		// inicializujeme stránkovač
		$paginator = new Paginator; // Paginator je třída z Nette Framework
		$paginator->itemsPerPage = 40;
		$paginator->itemCount = count($articles); // vrací celkový počet článků; ekvivalent k $articles->count()
		$paginator->page = $page;

		// a omezíme datasource na LIMIT a OFFSET
		$articles->applyLimit($paginator->length, $paginator->offset);

		// seznam článků předáme do šablony
		$this->template->articles = $articles;
	}

	...
}

Nakonec si šablona může říci, o které sloupce má zájem a může také nastavit vlastní řazení (kvůli čitelnosti jsem vynechal volání htmlSpecialChars):

<h1>Stručný přehled článků</h1>

<?php foreach ($articles->select(array('url', 'title', 'perex'))->orderBy('title') as $article):?>
	<h2><?= $article->title ?></h2>
	<p><a href="<?= $article->url ?>"><?= $article->perex ?></a></p>
<?php endforeach ?>

Klíčové je, že se provedou jen dva SQL dotazy, které si vyžádají jen skutečně vypisovaná data. V tomto případě se třeba nemusí přenášet celé texty článků, protože šablona vypisuje pouze perexy. Výsledkem je přehledný kód, optimalizované využítí databáze a neposkvrněná architektura MVC. Za zmínku také stojí, že dotaz na databázi se provádí až při volání count v presenteru a foreach v šabloně.

Doplnění: bohužel, pro MySQL to je nepoužitelné ☹