Cachování v Nette komponentách a latte šablonách 02/03/2016

Pojďme si ukázat několik způsobů jak cachovat data v jednotlivých Nette komponentách a jejich šablonách.

Cachování v komponentách se dá vyřešit mnoha způsoby, zkusím popsat všechny mně známé a vysvětlit, na co se hodí a kdy je dobré je použít.

Typy, o kterých budu mluvit.

  • cachování v šabloně
    • v místě použítí
    • v šabloně komponenty
  • cachování v komponentě
    • vynucené
    • volitelné
  • cachování při vytváření komponenty

Cachování v šabloně

-> v místě použití

use Nette\Application\UI\Control;

class Foo extends Control
{

    /**
     * @param mixed $list
     */
    public function __construct($list) {
        $this->list = $list;
    }

    public function render() {
        $this->template->list = $this->list;
        $this->template->setFile(__DIR__ . '/foo.latte');
        $this->template->render();
    }

}
protected function createComponentFoobar() {
    return new Foo($this->list);
}

Když máme $list jako pole, klasicky se nám hodí první způsob, a to cachovaní v šabloně.

Využijeme makra cache, kterému dáme unikátní název a vhodnou dobu expirace. Pro přípád pokročilého cachování, můžene využít i tagy.

Normálně se používá na většinu výpisů, klasicky při načítání z databáze jako alternativa ke cachování v komponentě nebo při vytváření komponenty.

// @layout.latte, default.latte, etc..

{cache 'foo/bar', expire => '+1 hour'}
    {control foobar}
{/cache}

-> v šabloně komponenty

use Nette\Application\UI\Control;

class Foo extends Control
{

    public function getList() {
        // some operations..
    }

    public function render()
        $this->template->list = $this->getList();
        $this->template->setFile(__DIR__ . '/foo.latte');
        $this->template->render();
    }

}
protected function createComponentFoobar() {
    return new Foo();
}

Cachování v šabloně komponety se obvykle používá, pokud máte jednoučelovou komponentu, která třeba počítá různé statistiky nebo vypisuje data s velkou časovou náročností.

// @layout.latte, default.latte, etc..
{control foobar}

// foo.latte
{cache 'foo', expire => '+1 day'}
    {foreach $list as $item}
        {* render *}
    {/foreach}
{/cache}

Cachování v komponentě

-> vynucené

use Nette\Application\UI\Control;
use Nette\Caching\IStorage;
use Nette\Caching\Cache;

class Foo extends Control
{

    /**
     * @param object $facade
     * @param IStorage $storage
     */
    public function __construct($facade, IStorage $storage) {
        $this->facade = $facade;
        $this->cache = new Cache($storage, 'foo');
    }

    /**
     * @return array
     */
    public function getCachedList() {
        return $this->cache->load('foo', function(& $dependencies) {
            $dependencies['expire'] = '+ 1 day';

            // some dark magic
            // ...

            return $list;
        });
    }

    public function render() {
        $this->template->list = $this->getCachedList();
        $this->template->setFile(__DIR__ . '/foo.latte');
        $this->template->render();
    }

}
protected function createComponentFoobar() {
    return new Foo($this->list);
}

Cachování v komponentě je defakto uplně stejné jako v šabloně, akorát se dějě na logicky jiném místě. Výhodu v tom vídím, že si člověk sám definuje, co chce cahovat, může si to vyladit přímo na míru, nastavit ruzné tagy, závislosti na jiných datech / souborech apod.

Mnozí by mohli argumentovat, že cachovat by se mělo jenom v komponentě a v šabloně nikoli. Že by to mohlo porušovat principy MVC. Já v tom takovou vědu nevidím. Každý si může využít to, co se mu hodí.

-> volitelné

use Nette\Application\UI\Control;
use Nette\Caching\IStorage;
use Nette\Caching\Cache;

class Foo extends Control
{

    /**
     * @param IStorage $storage
     */
    pubic function setCacheStorage(IStorage $storage) {
        $this->cache = new Cache($storage, 'foo');
    }

    public function render() {
        if ($this->cache) {
            $this->template->list = $this->getCachedList();
        } else {
            $this->template->list = $this->getList();
        }
        $this->template->setFile(__DIR__ . '/foo.latte');
        $this->template->render();
    }

}
protected function createComponentFoobar() {
    return new Foo($this->list);
}

V ideálním případě byste mohli cache nastavovat přes setter a komponenta už si sama rozhodne, zda-li cache využije nebo ne.

Pokud se vám použítí přes setter nelíbí, tak pro testovací učely je možné využít Nette\Caching\Storages\DevNullStorage, které simuluju cache, ale nic necachuje.

Cachování při vytváření komponenty

Tenhle způsob ja osobně moc nepoužívam, protože nerad špiním Presentery dalšími závislostmi.

Každopádně to lze také.

/**
 * @return array
 */
public function getCachedList() {
    return $this->cache->load('foo', function(& $dependencies) {
        $dependencies['expire'] = '+ 1 day';

        // some dark magic
        // ...

        return $list;
    });
}

protected function createComponentFoobar() {
    return new Foo($this->getCachedList());
}

V této variantě by bylo nejspíš lepší daná data cachovat ve facade / DAO / service. Na tento přípád se to hodí perfektně.


Naposledy, co jsem řešil cache bylo na Componette. Měl jsem data z databáze a chtěl jsem je cachovat, protože se skoro nemění. Rozhodl jsem se využít latte makra cache, aby to bylo pohodlné.

Moje komponenta vypadala nějak takto:

public function render()
{
    $this->template->categories = $this->tagRepository->findWithHighPriority();
    $this->template->tags = $this->ensure($this->tagRepository->findWithLowPriority());
    $this->template->setFile(__DIR__ . '/templates/menu.latte');
    $this->template->render();
}

A její šablona:

{cache menu, expire => '+1 day'}

{* rendering *}

{/cache}

Jenže to nefungovalo, jak jsem chtěl. Sice dotaz do databáze byl lazy-loading, ale já nad daty dělal nějakou chytristiku (vyhazoval položky z menu, které nemají připojený ani jeden tag) v metodě ensure.

Komponenta byla cachovaná, ale při renderování to dělalo vždy 1 dotaz.

Mohl jsem využít cachování v komponentě, ale nechtěl jsem další závislost. Mohl jsem cachovat v @layout.latte, ale já to chtěl mít přímo v šabloně komponenty.

Vyřešil jsem to tedy malým trikem.

public function render()
{
    $this->template->_categories = function () {
        return $this->tagRepository->findWithHighPriority();
    };
    $this->template->_tags = function () {
        return $this->ensure($this->tagRepository->findWithLowPriority());
    };
    $this->template->setFile(__DIR__ . '/templates/menu.latte');
    $this->template->render();
}

A její šablona:

{cache menu, expire => '+1 day'}

{var $categories => $_categories()}
{var $tags => $_tags()}

{* rendering *}

{/cache}

Je to velmi jednoduché, při vytváření cache mi anonymní funkce $_categories a $_tags vrácí správná data a ja je můžů krásně vyrenderovat.

Podruhé už se použije cache a člověk nemusí nic řešit.

Celkem tedy 0 dotazů. Mission complete.


Pokud máte nějaké vlastní typy, ukázky nebo workaroundy, budu rád, když se o ně podělíte.