Administrace textových stránek

Rudolf Svátek 2016-07-14 09:01

Celou aplikaci si opět můžeš stáhnout ve stavu, v jakém bude po dnešní kapitole: kapitola9.zip.

Opět potřebujeme vytvořit třídy pro Entitu, Repository a Mapper. Potom zaregistrovat službu v config.neon, vytvořit presenter a zobrazovací šablonu pro výpis stránek. Kvůli přidávání a editaci stránek budeme potřebovat ještě 2 formuláře s jejich šablonami. Tento princip bude stejný u všech uvažovaných modulů v administraci redakčního systému.

Entity

Entita jen opisuje MySQL tabulku. Co pole v tabulce, to metoda třídy. Interface IPagesEntity bude vyžadovat všechny metody, které pak třída PagesEntity bude implementovat. Takže moje IEntita pro stránky leží v souboru app\model\Pages\IPagesEntity.php a vypadá takto:

<?php

namespace App\Model;

interface IPagesEntity 
{

   public function id();
   public function parent();
   public function lft();
   public function rgt();
   public function level();
   public function title();
   public function name();
   public function menuTitle();
   public function url();
   public function description();
   public function keywords();
   public function perex();
   public function text();
   public function secret();
   public function secretText();
   public function lang();
   public function active();
   public function inMenu();
   public function onHomepage();
   public function date();
   public function upDate();
   public function pictureName();
   public function pictureDescription();
   public function galleryIds();

}

No a třída PagesEntity, která rozhraní implementuje app\model\Pages\IPagesEntity.php:

<?php

namespace App\Model;

class PagesEntity extends \Nette\Object implements IPagesEntity
{

   private $id;
   private $parent;
   private $lft;
   private $rgt;
   private $level;
   private $title;
   private $name;
   private $menuTitle;
   private $url;
   private $description;
   private $keywords;
   private $perex;
   private $text;
   private $secret;
   private $secretText;
   private $lang;
   private $active;
   private $inMenu;
   private $onHomepage;
   private $date;
   private $upDate;
   private $pictureName;
   private $pictureDescription;
   private $galleryIds;

    public function id($id = null) {
        if (!is_null($id))
            $this->id = $id;
        return $this->id;
    }

    public function parent($parent = null) {
        if (!is_null($parent))
            $this->parent = $parent;
        return $this->parent;
    }

    public function lft($lft = null) {
        if (!is_null($lft))
            $this->lft = $lft;
        return $this->lft;
    }

    public function rgt($rgt = null) {
        if (!is_null($rgt))
            $this->rgt = $rgt;
        return $this->rgt;
    }

    public function level($level = null) {
        if (!is_null($level))
            $this->level = $level;
        return $this->level;
    }

    public function title($title = null) {
        if (!is_null($title))
            $this->title = $title;
        return $this->title;
    }

    public function name($name = null) {
        if (!is_null($name))
            $this->name = $name;
        return $this->name;
    }

    public function menuTitle($menuTitle = null) {
        if (!is_null($menuTitle))
            $this->menuTitle = $menuTitle;
        return $this->menuTitle;
    }

    public function url($url = null) {
        if (!is_null($url))
            $this->url = $url;
        return $this->url;
    }

    public function description($description = null) {
        if (!is_null($description))
            $this->description = $description;
        return $this->description;
    }

    public function keywords($keywords = null) {
        if (!is_null($keywords))
            $this->keywords = $keywords;
        return $this->keywords;
    }

    public function perex($perex = null) {
        if (!is_null($perex))
            $this->perex = $perex;
        return $this->perex;
    }

    public function text($text = null) {
        if (!is_null($text))
            $this->text = $text;
        return $this->text;
    }

    public function secret($secret = null) {
        if (!is_null($secret))
            $this->secret = $secret;
        return $this->secret;
    }

    public function secretText($secretText = null) {
        if (!is_null($secretText))
            $this->secretText = $secretText;
        return $this->secretText;
    }

    public function lang($lang = null) {
        if (!is_null($lang))
            $this->lang = $lang;
        return $this->lang;
    }

    public function active($active = null) {
        if (!is_null($active))
            $this->active = $active;
        return $this->active;
    }

    public function inMenu($inMenu = null) {
        if (!is_null($inMenu))
            $this->inMenu = $inMenu;
        return $this->inMenu;
    }

    public function onHomepage($onHomepage = null) {
        if (!is_null($onHomepage))
            $this->onHomepage = $onHomepage;
        return $this->onHomepage;
    }

    public function date($date = null) {
        if (!is_null($date))
            $this->date = $date;
        return $this->date;
    }

    public function upDate($upDate = null) {
        if (!is_null($upDate))
            $this->upDate = $upDate;
        return $this->upDate;
    }

    public function pictureName($pictureName = null) {
        if (!is_null($pictureName))
            $this->pictureName = $pictureName;
        return $this->pictureName;
    }

    public function pictureDescription($pictureDescription = null) {
        if (!is_null($pictureDescription))
            $this->pictureDescription = $pictureDescription;
        return $this->pictureDescription;
    }

    public function galleryIds($galleryIds = null) {
        if (!is_null($galleryIds))
            $this->galleryIds = $galleryIds;
        return $this->galleryIds;
    }

}

Pro ukládání hierarchických struktur používám metodu traverzování okolo stromu, tak proto jsou v tabulce kromě pole parent i pole lft a rgt. Používám i pole level, abych viděl jaké zanoření ve stromové struktuře stránka má. Pak je tam pole secret. To používám na stránky, které mají vidět jen přihlášení uživatelé. Nepřihlášení stránku vůbec neuvidí. Podobné pole je secretText. To použiju na obsah, který se ukáže jen přihlášenému uživateli. Nepřihlášený stránku normálně uvidí, ale uvidí jen veřejný text a tajný text ne.

Pole lang má sloužit k rozlišení jazykové mutace stránky. V tuto chvíli zatím nemám zpracovánu podporu pro vícejazyčný web a navíc vím, že jakmile se do toho pustím, musím to vymyslet jinak, ale kvůli kompatibilitě tam to pole mám.

Pole inMenu bude obsahovat senam různých menu, do kterých bude stránka patřit.Opět vím, že programátorsky to není úplně správně a čistě, ale podřídil jsem se hlavnímu požadavku - snadné obsluze i nezkušeným uživatelem. Normálně by měl uživatel vytvářet menu odděleně od stránek a to přijde uživatelům nesnadné. Já vytvořil čtyři menu a každá stránka může být zařazena do každého z nich (nebo žádného).

Zbytek polí je celkem klasika - id, textový obsah, titulky, název apod.

Mapper

Nějaký mapper jsme psali už v kapitole 5. Model pro tabulku Users. Ten byl jednoduchý a kromě konstruktoru nepotřeboval nic. U stránek to bude složitější. Zejména kvůli tomu traverzování. Nejde třeba jen tak smazat stránku. Po smazání se totiž musí přečíslovat indexy lft a rgt, abychom udrželi strom konzistentní. Nebo nechci dovolit smazat stránku, která má nějaké podstránky. Podobně musím řešit různé přepočty při změnách pořadí stránek atd. Většina operací, které musím v Mapperu řešit, se týká přesouvání stránek při změně pořadí.

Takže vyrob soubor app\model\Pages\PagesMapper.php. Začátek je:

<?php

namespace App\Model;

use Dibi\Connection;

class PagesMapper extends BaseMapper
{

	/**
	 * @param Connection $db
	 */
	public function __construct(Connection $db) {
		parent::__construct($db);
	}

Takže jen injectneme Dibi\Connection, abychom mohli pracovat s databází.

Přesunutí stromu stránek

Na začátku vím, jakou stránku přesouvám a jakým směrem. Pokud má stránka nějaké potomky, pak přesouvám celou skupinu stránek. Podle směru přesouvání zjistím předchozí, nebo následující stránku, případně zase celou skupinu stránek. A pak musím přečíslovat obě skupiny stránek. Nepřišel jsem zatím na to, jak to udělat jedním dotazem, tak používám 3. Pro přesun nahoru, či dolů, slouží 2 funkce:

	/**
	 * @param PagesEntity $page
	 * @param PagesEntity $neighborPage
	 */
	public function move(PagesEntity $page, PagesEntity $neighborPage) {
		$differentPage = (integer)$page->rgt() - (integer)$page->lft() + 1;
		$differentNeighbor = (integer)$neighborPage->rgt() - (integer)$neighborPage->lft() + 1;
		$min_lft = min($page->lft(), $neighborPage->lft());
		$max_rgt = max($page->rgt(), $neighborPage->rgt());
		switch ($page->lft() < $neighborPage->lft()) {
			case true:
				$lft = $page->lft();
				$rgt = $page->rgt();
				break;
			case false:
			default:
				$lft = $neighborPage->lft();
				$rgt = $neighborPage->rgt();
				$differentPage = $differentNeighbor;
				$differentNeighbor = $differentPage;
				break;
		}

		$sql = "UPDATE pages
	        SET lft = lft + IF(@subtree := lft >= " . $lft . " AND rgt <= " . $rgt . ", $differentNeighbor, 
	                IF(lft >= " . $min_lft . ", -$differentPage, 0)),
	            rgt = rgt + IF(@subtree, $differentNeighbor, IF(rgt <= " . $max_rgt . ", -$differentPage, 0))
	        WHERE rgt >= " . $min_lft . " AND lft <= " . $max_rgt . "";

		$this->db->query($sql);
	}

Jde to jedním dotazem, což je fajn. Je ale trochu pokročilejší, takže vysvětlím. Přesouvaná stránka může mít nějaké potomky a pak přesouvám celou skupinu. Ať už je sama, nebo je to celá skupina, budu ji označovat jako přesouvaná skupina. Přesouvám buď nahoru, nebo dolu a vždy daným směrem musí být nějaká sousední stránka, která také může obsahovat potomky. Budu ji označovat jako sousední skupina.

Tak nejdřív zjistím, kolik stránek tvoří obě skupiny. To jsou ty proměnné $differentPage a $differentNeighbor.

Pak také zjistím kolik stránek vlastně budu potřebovat přečíslovat. To zjistím tak, že najdu nejmenší lft a největší rgt z obou skupin. Tyto hodnoty pak použijeme v podmínce SQL dotazu.

Dalším krokem je určit, zda se přesouvá nahoru, či dolů. Stačí porovnat lft obou skupin. Pokud je lft přesouvané stránky menší, pak leží nad sousední stránkou a přesouvá se dolů. 

Pak naplním proměnné $lft a $rgt. V případě, že se přesouvá nahoru, ještě přehodím hodnoty tak, jako bych vlastně přesouval sousední skupinu dolů.

V tom SQL dotazu nejprve naplním proměnnou @subtree. To bude vždy skupina, která se přesouvá dolů. Stránkám v @subtree pak k jejich lft přičtu hodnotu $differentNeighbor. Stránkám, které nejsou v @subtree od jejich lft odečtu hodnotu $differentPage. no a stejnou operaci provedu s indexem rgt.

Změna rodiče

Speciální případ přesunu stránek je změna rodiče stránky. Při použití traverzování okolo stromu je nutné, aby každá stránka nějakého rodiče měla. Výjimkou je kořenová stránka, která rodiče nemá a sama obsahuje celý strom. Při změně rodiče se musí pamatovat na spousty věcí a je to asi nejsložitější úkon v traverzování.

	/**
	 * @param PagesEntity $page
	 * @param PagesEntity $newParent
	 */
	public function changeParent(PagesEntity $page, PagesEntity $newParent) {
		$different = $page->rgt() - $page->lft() + 1;
		$lft = $newParent->rgt();
		$level = $newParent->level() + 1;
		if ($lft > $page->lft())
			$lft -= $different;

		$min_lft = min($lft, $page->lft());
		$max_rgt = max($lft + $different - 1, $page->rgt());
		$move = $lft - $page->lft();
		if ($lft > $page->lft())
			$different = -$different;

		$sql = "UPDATE $this->tableName
	        SET level = level + IF(@subtree := lft >= " . $page->lft() . " 
	                AND rgt <= " . $page->rgt() . ", " . ($level - $page->level()) . ", 0),
	            parent = IF(id = " . $page->id() . ", " . $newParent->id() . ", parent),
	            lft = lft + IF(@subtree, $move, IF(lft >= $min_lft, $different, 0)),
	            rgt = rgt + IF(@subtree, $move, IF(rgt <= $max_rgt, $different, 0))
	        WHERE rgt >= $min_lft AND lft <= $max_rgt ";

		$this->db->query($sql);
	}

Takže víme o jakou stránku se jedná a kdo má být jejím novým rodičem. To jsou parametry, které metoda přijímá. Nejprve zjistíme rozdíl mezi rgt a lft a tím dostaneme celý přesouvaný strom. Hodnotu uložíme do proměnné $different. Do proměnné $lft si dočasně uložíme hodnotu rgt nového rodiče a také si řekneme, že přesouvaná stránka bude mít $level o jedna vyšší než její budoucí rodič.

Pak je tam podmínka, kdy porovnávám to dočasné $lft s lft přesouvané stránky. Pokud je větší, leží nový rodič pod přesouvanou stránkou a jakmile přesouvaná skupina zmizne (aby se pak objevila jinde), musím všechny stránky, co byly pod ní, posunout nahoru. To znamená zmenšit jejich lft a rgt. Proto tedy říkám, že proměnná $lft je dočasná, protože tady se může změnit, když od ní odečítám hodnotu $different:

		if ($lft > $page->lft())
			$lft -= $different;

Zjistím rozsah stránek, kterých se přečíslování dotkne. Budou to stránky, které sahají od nejmenšího lft po největší rgt. O kolik posunu indexy v přesouvaném stromu, to uložím do proměnné $move. Nakonec, pokud je $lft stále ještě menší než lft přesouvané stránky, bude mít hodnota $different opačné znaménko.

SQL dotaz pak už jen do proměnné @subtree vloží rozsah přesouvaných stránek a upraví jim level a parent. Indexy lft a rgt přepočítá tak, že přičte hodnotu $move pro stránky v @subtree. U ostatních stránek použije hodnotu $different.

Jak říkám - celkem složité na vysvětlení, možná i na pochopení, ale jestli chceš, zkus si chvilku hrát s debugem a uvidíš proč a jak to funguje.

Mazání stránek

Další metoda, která se týká přečíslování indexů, se používá při mazání. Redakční sytém jsem navrhl tak, aby se daly mazat jen takové stránky, které nemají žádného potomka. Takové stránky mají vždy rgt o jedna větší než lft. Stačí tedy najít všechny stránky, které mají větší lft než smazaná stránka a zmenšit jejich lft a rgt o 2. Následně můžu stránku smazat.

	/**
	 * @param PagesEntity $page
	 */
	public function prepareBeforeDelete(PagesEntity $page) {
		$sql = "UPDATE $this->tableName
	        SET 
	        lft = lft - IF(lft > " . $page->rgt() . ", 2, 0),
	        rgt = rgt - IF(rgt > " . $page->rgt() . ", 2, 0)
	         ";
		$this->db->query($sql);
	}

Samotné mazání zajišťuje BaseMapper, takže tady to už není nutné řešit. Prostě snížím lft i rgt o 2 u stránek, které leží za mazanou stránkou (mazanou proto, že ji mažu, ne, že ja tak protřelá :-)

Přidání stránky

Jakmile stránku přidávám, musím opět přepočítat indexy lft a rgt u všech stránek, která tím budou ovlivněny. Pro jednoduchost budeme řadit novou stránku vždy jako poslední ve skupině. Každá nová stránka musí mít nějakého rodiče, Takže jejímu rodiči a vlastně všem v řadě až k rootu, musím zvýšit jejich rgt o 2. Navíc ale také musím všem stránkám, které následují za novou stránkou zvýšit oba indexy o 2. Zařídí to funkce:

	/**
	 * @param PagesEntity $page
	 */
	public function prepareBeforeAdd(PagesEntity $page) {
		$sql = "UPDATE $this->tableName
	        SET 
	        lft = lft + IF(lft > " . $page->rgt() . ", 2, 0),
	        rgt = rgt + IF(rgt >= " . $page->rgt() . ", 2, 0)
	         ";
		$this->db->query($sql);
	}

Vložení samotné zase řeší BaseMapper.

Další metody

Pak jsou 3 metody, které s přesouváním nesouvisí. Jde o výběr stránek do menu:

	/**
	 * @param $menuType
	 * @return array
	 */
	public function getPagesInMenu($menuType) {
		return $this->db->select('id, url, name, IF(LENGTH(menuTitle) > 0, menuTitle, name) as menuTitle, parent, level')
						->from($this->tableName)
						->where('inMenu LIKE %~like~', $menuType)
						->where('active = %i', 1)
						->groupBy('inMenu, url')
						->orderBy('lft')
						->fetchAll();
	}

pak hledání stránek podle zadaného výrazu:

	/**
	 * @param $term
	 * @return \Dibi\Fluent
	 */
	public function search($term) {
		return $this->db->select('*')
						->from($this->tableName)
						->where("MATCH (name,title,perex,text) AGAINST ('*' %s '*' IN BOOLEAN MODE)", $term);
	}

a nakonec výběr stránek, které by připadaly v úvahu pro výběr rodiče v editaci stránky:

	/**
	 * @param PagesEntity $page
	 * @return array
	 */
	public function getPosibleParentsTree(PagesEntity $page) {
		return $this->db->select('id, CONCAT(REPEAT(\'- \', level), name) as name')
						->from($this->tableName)
						->where('lft < %i OR rgt > %i', $page->lft(), $page->rgt())
						->orderBy('lft')
						->fetchPairs();
	}

Samozřejmě, pokud tě napadne cokoli smysluplného, můžeš si to do Mapperu přidat.

Repository

Poslední soubor modelu leží v app\models\Pages\PagesRepository.php. Začátek repozitáře bude vypadat takto:

<?php

namespace App\Model;

use App\Model\PagesEntity;

class PagesRepository extends BaseRepository
{

	/** @var PagesMapper */
	private $mapper;

	const UP = 'up';
	const DN = 'dn';

	/**
	 * PagesRepository constructor.
	 * @param PagesMapper $mapper
	 */
	public function __construct(PagesMapper $mapper) {
		parent::__construct($mapper);
		$this->mapper = $mapper;
	}

Věřím, že to podrobné vysvětlení nepotřebuje. Dědíme z BaseRepository, kde už jsou metody pro výběr, ukládání, mazání atd. V constructoru si připravíme mapper. Ještě deklaruji 2 konstanty pro rozlišení směru přesouvání stránek.

Přesouvání

Pro přesun jsme v Mapperu dělali metodu move(). V repositáři ji budeme volat s definovaným směrem přesunu:

	/**
	 * @param $id
	 */
	public function move($id, $direction = self::DN) {
		$page = $this->get($id);
		if($direction == self::DN)
			$neighborPage = $this->getOneWhere(['lft' => $page->rgt() + 1]);
		else
			$neighborPage = $this->getOneWhere(['rgt' => $page->lft() - 1]);
		$this->mapper->move($page, $neighborPage);
	}

Změna rodiče

Tady jen zjistíme nového rodiče a voláme mapper:

	/**
	 * @param $page
	 * @param $values
	 */
	public function changeParent($page, $values) {
		$newParent = $this->get($values->parent);
		$this->mapper->changeParent($page, $newParent);
	}

Mazání stránky

V případě stránek jsme ale už zjistili, že třeba mazání nemůže proběhnout jen odstraněním jednoho řádku v tabulce. Ještě se musejí přepočítat indexy. Ty se přepočítávají vždy, když dojde k nějaké změně - přidání nové stránky, změna řazení a mazání. Takže přetížíme metodu delete z BaseRepository takto:

	/**
	 * @param $id
	 * @return bool
	 */
	public function delete($id) {
		/** @var PagesEntity $page */
		$page = $this->get($id);

		if ($this->haveChild()) {
			$this->flashMessage('Stránku ' . (string) $this->page->title() .
					' nelze smazat - obsahuje podřízené stránky.', 'danger');
			$this->redirect('default');
		}

		$this->begin();
		try {
			$this->mapper->prepareBeforeDelete($this->page);
			$this->mapper->delete($this->page->id());
		} catch (Exception $ex) {
			$this->rollback();
			return false;
		}
		$this->commit();
		return TRUE;
	}

To si malé vysvětlení zaslouží. Nejprve zjistím, zda existují nějaké podřízené stránky díky volání metody haveChild(). Úmyslně jsem redakční systém navrhnul tak, aby nešlo mazat celé stromy stránek. Nic tak nezamrzí, jako když se překlikneš a smažeš si 20 stránek místo jedné. Pokud má stránka potomky, vrátím FALSE a nic nemažu. Jestli mazaná stránka potomky nemá, pak zkusím přečíslovat indexy pomocí metody prepareBeforeDelete. V tomto okamžiku se vlastně databáze ocitne v nekonziztentním stavu, protože najenou existují 2 stránky se stejnými indexy lft a rgt. Jakmile stránku smažu pomocí volání delete(), bude vše napraveno.

Přečíslování a smazání musí být provedeno v jednom kroku, tak proto používám transakce.

Metoda haveChild je jednoduchá. Když je rozdíl mezi rgt a lft jiný než 1, budou nějací potomci:

	/**
	 * return int
	 */
	private function haveChild() {
		return $this->page->rgt() - $this->page->lft() - 1;
	}

Přidání stránky

Opět jen voláme mětodu mapperu:

	public function prepareBeforeAdd($parent) {
		$this->mapper->prepareBeforeAdd($parent);
	}

Ostatní metody

Pak tu máme zase metody, které nesouvisí s traverzováním. První je pro výběr stránek, kdy nejprve zjistíme, zda je uživatel přihlášen, a zda tedy může vidět stránky s příznakem secret. Ať už přihlášen je, či není, ukážeme jen takové stránky, které jsou nastaveny jako aktivní.

	/**
	 * @return \Dibi\Fluent
	 */
	public function getFrontAll() {
		$pages = $this->mapper->getAll()
				->where(['active' => 1]);

		if (!$this->user->isLoggedIn())
			$pages->where(['secret' => 0]);

		return $pages;
	}

Metoda, která se bude volat po odeslání formuláře pro vyhledávání:

	/**
	 * @param $term
	 * @return \Dibi\Fluent
	 */
	public function search($term) {
		return $this->mapper->search($term);
	}
No a poslední metoda, která vybere stránky do menu:
	/**
	 * @param $menuType
	 * @return array
	 */
	public function getPagesInMenu($menuType) {
		return $this->mapper->getPagesInMenu($menuType);
	}

Nastavení služby

Uprav soubor app\config\config.neon tak, že do sekce services přidáš řádky:

	pagesMapper: App\Model\PagesMapper
	- App\Model\PagesRepository(@pagesMapper)

Od té chvíle můžeš používat službu PagesRepository. Tu budeš potřebovat v presenteru.

Presenter

Presentery by měly být co nejjednodušší. V případě administrace stránek se to ale přeci jen malinko hůř dodržuje. Nějakou logiku tam dát musíme. Takže presenter bude v app\AdminModule\presenters\PagesPresenter.php a začátek bude:

<?php

namespace App\AdminModule\Presenters;

use App\Model\PagesEntity;
use App\Model\PagesRepository;
use Grido\Grid;
use Nette\Application\UI\Form;
use Nette\Utils\Html;
use PDOException;
use Nette\Utils\DateTime;

/**
 * Class PagesPresenter
 * @package App\AdminModule\Presenters
 */
class PagesPresenter extends BasePresenter
{

	/** @var PagesRepository @inject */
	public $pagesRepository;

	const UP = 'up';
	const DN = 'dn';

Vidíš definice nějakých konstant UP a DN a injektáž služby PagesRepository. Zatím nic zajímavého.

Vždy by měla existovat hlavní kořenová stránka. Ta nemá žádného rodiče a sama je rodičem všech ostatních stránek. Proto zajistíme, aby se vytvořila sama, pokud žádná neexistuje. Poslouží nám k tomu metoda startup() v presenteru.

	public function startup() {
		parent::startup();
		$pages = $this->pagesRepository->getAll()->fetch();
		if ($pages === FALSE) {
			$page = new PagesEntity;
			$page->level(0);
			$page->lft(1);
			$page->rgt(2);
			$page->parent(0);
			$page->active(1);
			$page->date(new DateTime);
			$page->upDate(new DateTime);
			$page->name('root');
			$page->menuTitle('root');
			$page->title('root');
			$page->url('/');
			$this->pagesRepository->save($page);
		}
	}

Pole name, menuTitle, a Url si uprav podle sebe. Dále vytvoříme komponentu pro výpis stránek. Chceme to udělat tak, aby ve výpisu byla tlačítka pro různé akce - editaci, mazání, přesuny stránek atd.

Výpis stránek

V presenteru tedy vznikne metoda, která využívá Grido:

	/**
	 * @param $name
	 * @return Grid
	 */
	protected function createComponentGrid($name) {
		$grid = new Grid($this, $name);
		$grid->translator->lang = 'cs';

		$fluent = $this->pagesRepository->getAll();

		foreach ($fluent as $pageRow)
			$pages[$pageRow->id] = $pageRow;

		if (isset($grid->model))
			$grid->model = $fluent;

		$grid->addColumnNumber('id', 'ID');
		$header = $grid->getColumn('id')->headerPrototype;
		$header->style['width'] = '0.5%';

		$grid->addColumnText('name', 'Název')
				->setFilterText()
				->setSuggestion();
		$grid->getColumn('name')->headerPrototype->style['width'] = '40%';

		$grid->addColumnText('active', 'Aktivní')
						->setCustomRender(function ($item) {
							if ($item->active == 0) {
								$i = Html::el('i', ['class' => 'glyphicon glyphicon-thumbs-down']);
								$el = Html::el('a', ['class' => 'btn btn-danger btn-xs btn-mini ajax'])
										->href($this->presenter->link("active!", $item->id))
										->setHtml($i);
							} else {
								$i = Html::el('i', ['class' => 'glyphicon glyphicon-thumbs-up']);
								$el = Html::el('a', ['class' => 'btn btn-success btn-xs btn-mini ajax'])
										->href($this->presenter->link("active!", $item->id))
										->setHtml($i);
							}
							return $el;
						})
				->cellPrototype->class[] = 'center';

		$grid->addColumnText('inMenu', 'Top menu')
						->setCustomRender(function ($item) {
							$menuItems = (array) json_decode($item->inMenu);
							if (!in_array('topMenu', $menuItems)) {
								$i = Html::el('i', ['class' => 'glyphicon glyphicon-thumbs-down']);
								$el = Html::el('a', ['class' => 'btn btn-danger btn-xs btn-mini ajax'])
										->href($this->presenter->link("inTopMenu!", $item->id))
										->setHtml($i);
							} else {
								$i = Html::el('i', ['class' => 'glyphicon glyphicon-thumbs-up']);
								$el = Html::el('a', ['class' => 'btn btn-success btn-xs btn-mini ajax'])
										->href($this->presenter->link("inTopMenu!", $item->id))
										->setHtml($i);
							}
							return $el;
						})
				->cellPrototype->class[] = 'center';

		$grid->addActionHref('edit', '')
				->setIcon('pencil');

		$grid->addActionEvent('delete', '')
				->setCustomRender(function ($item) {
					if ($item->parent === 0 || $item->rgt - $item->lft > 1) {
						$i = Html::el('i', ['class' => 'fa']);
						$el = Html::el('span', ['class' => 'btn btn-xs btn-mini'])->setHtml($i);
					} else {
						$i = Html::el('i', ['class' => 'fa fa-trash']);
						$el = Html::el('a', ['class' => 'btn btn-default btn-xs btn-mini ajax', 'data-grido-confirm' => "Opravdu chcete tuto položku odstranit?"])
								->href($this->presenter->link("delete!", $item->id))
								->setHtml($i);
					}
					return $el;
				});

		$grid->addActionEvent('moveup', '')
				->setCustomRender(function ($item) use ($pages) {
					$first = $item->parent == 0 || ($item->lft - 1) == $pages[$item->parent]['lft'];

					if ($first) {
						$i = Html::el('i', ['class' => 'fa']);
						$el = Html::el('span', ['class' => 'btn btn-xs btn-mini'])->setHtml($i);
					} else {
						$i = Html::el('i', ['class' => 'fa fa-arrow-up']);
						$el = Html::el('a', ['class' => 'btn btn-default btn-xs btn-mini ajax'])
								->href($this->presenter->link("move!", $item->id, self::UP))
								->setHtml($i);
					}
					return $el;
				});

		$grid->addActionEvent('movedn', '')
				->setCustomRender(function ($item) use ($pages) {
					$last = $item->parent == 0 || ($item->rgt + 1) == $pages[$item->parent]['rgt'];

					if ($last) {
						$i = Html::el('i', ['class' => 'fa']);
						$el = Html::el('span', ['class' => 'btn btn-xs btn-mini'])->setHtml($i);
					} else {
						$i = Html::el('i', ['class' => 'fa fa-arrow-down']);
						$el = Html::el('a', ['class' => 'btn btn-default btn-xs btn-mini ajax'])
								->href($this->presenter->link("movedn!", $item->id, self::DN))
								->setHtml($i);
					}
					return $el;
				});

		$grid->addActionHref('add', '')
				->setIcon('plus');

		$grid->setDefaultSort([
			'lft' => 'ASC',
		]);
		$grid->setRememberState(TRUE);
		$grid->setPrimaryKey('id');
		$grid->setDefaultPerPage(50);
		$grid->setRowCallback(function ($item, $tr) {
			$tr->class[] = "level_{$item->level}";
			return $tr;
		});

		$grid->filterRenderType = $this->filterRenderType;
		$grid->setExport();

		return $grid;
	}

Věřím, že ve spolupráci s dokumentací na stránkách Grida poznáš proč jsem to napsal takto. Chci mít prostě výpis stránek, kde uvidím jejich id, název, informaci zda je stránka aktivní a součástí horního menu, plus tlačítka pro editaci, mazání a přesuny stránek.

Snad jen zmíním malou vychytávku v eventech $grid->addActionEvent('moveup', '')  a $grid->addActionEvent('movedn', ''). Volám tam anonymní funkci  ->setCustomRender(function ($item) use ($pages) pro vykreslení tlačítek pro přesouvání. Přesouvat lze ale pouze stránky, které daným směrem ještě mají nějakou další stránku. Když už je stránka první ve své skupině, nahoru ji nelze přesunout. Podobně dolů nepřesunu poslední stránku. Hlavní stránku "root" nelze přesunout nikdy. Proto tlačítka pro přesun zobrazím jen tam, kde to má smysl.

Také tlačítko pro mazání zobrazím jen tehdy, když stránka nemá žádné potomky.

Zjišťuji to zase porovnáváním indexů rgt a lft.

V Gridu se volají nějaké eventy a callbacky. K nim tedy musí vzniknout odpovídající metody.

Tak nejprve metoda, která nastaví stránku jako aktivní, nebo ji naopak vypne:

	/**
	 * @param $id
	 */
	public function handleActive($id) {
		$page = $this->pagesRepository->get($id);
		$page->active(!$page->active());
		$this->pagesRepository->save($page);
		if (!$this->isAjax())
			$this->redirect('default');
		$this->redrawControl();
	}

Jednoduše vezme stránku a nastaví jí vlastnost "active" na opačnou než má teď. Pokud je požadavek poslán ajaxově, překreslí se jen grid a nemusí se načítat celá stránka. O ajaxu někdy příště. Zatím je to tu připraveno k použití.

Další funkcí může být zařazení stránky do horního menu, nebo její vyřazení. Každá stránka může být zařazena do jednoho až čtyřech menu. Případně nemusí být v žádném z nich.

	/**
	 * @param $id
	 */
	public function handleInTopMenu($id) {
		$page = $this->pagesRepository->get($id);
		$items = json_decode($page->inMenu(), true);
		$key = array_search('topMenu', $items);

		if (is_numeric($key))
			unset($items[$key]);
		else
			array_push($items, 'topMenu');

		$page->inMenu(json_encode($items));
		$this->pagesRepository->save($page);

		if (!$this->isAjax())
			$this->redirect('default');
		$this->redrawControl();
	}

Nejdříve zjistím v jakých menu je stránka zařazena. Ukládám si to v json, tak proto data získám do pole pomocí json_decode($page->inMenu(), true). Pokud v poli bude topMenu, tak tento prvek pole odstraním a jako json zase uložím. Jestli tam nebude, tak ho přdám a opět uložím jako json. Čili zase přepínač jako v případě nastavení aktivní/neaktivní.

A teď další případ a to je smazání stránky. Už jsme to ošetřovali v modelu. V presenteru tedy jen zavoláme repository:

	/**
	 * @param $id
	 */
	public function handleDelete($id) {
		$this->pagesRepository->delete($id);
		if (!$this->isAjax())
			$this->redirect('default');
		$this->redrawControl();
	}

Přesuny stránek

Veškerou logiku pro přesuny jsme už opět dělali v modulu. V presenteru tedy jen zavoláme metodu, které ten přesun zařídí:

	/**
	 * @param $id
	 */
	public function handleMove($id, $direction = self::DN) {
		$this->pagesRepository->move($id, $direction);
		if (!$this->isAjax())
			$this->redirect('default');
		$this->redrawControl();
	}

Pro výpis stránek ještě potřebujeme šablonu, takže vytvoř soubor app\AdminModule\presenters\templates\Pages\default.latte a vlož pár řádků:

{block content}
	<div class="row">
		<div class="col-sm-12 form-group">
			<a n:href="Pages:add" class="btn btn-success btn-sm">
				<span class="glyphicon glyphicon-plus"></span> {_ messages.pages.add}
			</a>
		</div>
	</div>

	{snippet grid}
		{control grid}
	{/snippet}
{/block}

{block title}{_'messages.pages.admin'} |&nbsp;{/block}

Šablona odkazuje na "Pages:add", což je přidání stránky. To ještě nemáme vyřešené, takže při zobrazení by to teď vypsalo chybu. Také tam vidíš, že komponenta "grid" je obalana do snippetu. To je příprava na ajaxové zpracování. Poslední upozornění se týká definice bloku "title". Ten používám pro nastavení titulku prohlížeče. Všimni si, že je ve tvaru pro překladač.nezapomeň přidat příslušný výraz do souboru s překladem.

Editace stránky

Pro editaci musíme zase vytvořit formulář, zobrazit jej a po odeslání zpracovat. Prezenter musí obsahovat metody pro vytvoření formulářové komponenty, taky metodu, která vyplní editovanou stránku jejími současnými hodnotami a samozřejmě metodu pro zpracování odeslaného formuláře.

Tak přidáme metodu pro zobrazení a akci "edit":

	/**
	 * @param int $id
	 */
	public function renderEdit($id = 0) {
		/** @var Form $form */
		$form = $this['editForm'];
		if (!$form->isSubmitted()) {
			$item = $this->pagesRepository->get($id);
			$row = $this->pagesRepository->itemToArray($item);
			if (!$row)
				throw new PDOException('Záznam nenalezen');
			$row['inMenu'] = json_decode($row['inMenu']);
			$form->setDefaults($row);
			$this->getTemplate()->picture = $item->pictureName();
		}
	}

	public function actionEdit($id = NULL) {
		$this->page = $this->pagesRepository->get($id);
	}

V metodě renderEdit zjistíme o jakou stránku se jedná a vyplníme editační formulář zjištěnými hodnotami. V metodě actionEdit pak vytvoříme objekt, který reprezentuje editovanou stránku. Budeme ho potřebovat při vytváření komponenty formuláře kvůli tomu, abychom si mohli vypsat selectbox s výběrem potencionálních rodičů stránky.

Budeš tedypotřebovat samotnou komponentu formuláře. Při volání create se předává $this->page:

	/**
	 * @return Form
	 */
	protected function createComponentEditForm() {
		$form = $this->editPageFormFactory->create($this->page);
		$form->onSuccess[] = function (Form $form) {
			$this->presenter->flashMessage('Stránka byla uložena.', 'success');
			if (isset($form->submitted->name) && $form->submitted->name == 'saveandstay')
				$this->redirect('edit', $form->values->id);

			$this->redirect('default');
		};
		return $form;
	}

Nějaké FormFactory jsme už viděli u uživatelů. Takže vyrobíme formulář i pro stránky. Vznikne soubor app\AdminModule\components\EditPageFormFactory.php. Celý jeho obsah:

<?php

namespace App\Forms;

use App\Model\PagesEntity;
use App\Model\PagesRepository;
use Nette\Application\UI\Form;
use Nette\Utils\DateTime;
use Nette\Utils\Strings;
use Kdyby\BootstrapFormRenderer\BootstrapRenderer;

/**
 * Class EditPageFormFactory
 * @package App\Forms
 */
class EditPageFormFactory
{

	/** @var FormFactory */
	private $factory;

	/** @var PagesEntity */
	private $page;

	/** @var PagesRepository */
	private $pagesRepository;

	public function __construct(FormFactory $factory, PagesRepository $pagesRepository) {
		$this->factory = $factory;
		$this->pagesRepository = $pagesRepository;
	}

	/**
	 * @param PagesEntity $page
	 * @return Form
	 */
	public function create(PagesEntity $page) {
		$form = $this->factory->create();
		
		$posibleParentsTree = $this->pagesRepository->getPosibleParentsTree($page);

		$form->addHidden('id', 'ID');
		$form->addText('name', 'Název')
				->setAttribute('placeholder', 'Název stránky')
				->setAttribute('class', 'form-control input-sm')
				->setRequired('Zadejte prosím Název');
		$form->addText('url', 'URL')
				->setAttribute('class', 'form-control input-sm')
				->setAttribute('placeholder', 'URL stránky');
		$form->addCheckbox('onHomepage', 'Na úvodní stranu')
				->setAttribute('class', 'bootstrap');
		$form->addCheckboxList('inMenu', 'Položka menu:', [
					'topMenu' => 'Horní menu',
					'footerMenu' => 'Menu v patičce',
					'sideMenu' => 'Postranní menu',
					'otherMenu' => 'Jiné menu',
				])
				->setAttribute('class', 'bootstrap');
		$form->addCheckbox('active', 'Aktivní')
				->setAttribute('class', 'bootstrap');
		$form->addCheckbox('secret', 'Pouze pro přihlášené')
				->setAttribute('class', 'bootstrap');
		$form->addText('menuTitle', 'Název do menu')
				->setAttribute('class', 'form-control input-sm')
				->setAttribute('placeholder', 'Název v menu');
		$form->addText('title', 'Titulek')
				->setAttribute('class', 'form-control input-sm')
				->setAttribute('placeholder', 'Titulek v prohlížeči');
		$form->addText('description', 'Description')
				->setAttribute('class', 'form-control input-sm')
				->setAttribute('placeholder', 'Popisek');
		$form->addText('keywords', 'Keywords')
				->setAttribute('class', 'form-control input-sm')
				->setAttribute('placeholder', 'Klíčová slova');
		$form->addTextArea('perex', 'Perex')
				->setAttribute('class', 'form-control input-sm')
				->setAttribute('placeholder', 'Perex');

		$form->addTextArea('text', 'Text')
				->setAttribute('placeholder', 'Text stránky');
		$form->addTextArea('secretText', 'Text po přihlášení')
				->setAttribute('placeholder', 'Text pro přihlášené uživatele');
		$form->addText('pictureName', 'Obrázek')
				->setAttribute('class', 'form-control input-sm')
				->setAttribute('placeholder', 'Připojený obrázek')
				->setAttribute('onclick', 'openKCFinder(this)');
		$form->addText('pictureDescription', 'Popisek obrázku')
				->setAttribute('class', 'form-control input-sm')
				->setAttribute('placeholder', 'Popisek obrázku');
		$form->addSelect('parent', 'Rodič', $posibleParentsTree)
				->setAttribute('class', 'form-control input-sm select2')
				->setAttribute('placeholder', 'Rodič');
		$form->addSubmit('save', 'Uložit')
				->setAttribute('class', 'btn btn-primary');
		$form->addSubmit('saveandstay', 'Uložit a zůstat')
				->setAttribute('class', 'btn btn-default');

		$form->setRenderer(new BootstrapRenderer);
		$form->getElementPrototype()->class('form-horizontal');

		$form->onSuccess[] = function (Form $form) {
			$this->formSubmitted($form);
		};
		return $form;
	}

	/**
	 * @param $form
	 */
	public function formSubmitted(Form $form) {
		$values = $form->getValues();
		$this->page = $this->pagesRepository->get($values->id);
		$this->page->name($values->name);
		$this->page->inMenu(json_encode($values->inMenu));
		$this->page->menuTitle($values->menuTitle);
		$this->page->perex($values->perex);
		$this->page->text($values->text);
		$this->page->pictureName($values->pictureName);
		$this->page->pictureDescription($values->pictureDescription);
		$this->page->active($values->active);
		$this->page->onHomepage($values->onHomepage);
		$this->page->secret($values->secret);
		$this->page->title($values->title);
		$this->page->description($values->description);
		$this->page->keywords($values->keywords);
		$this->page->secretText($values->secretText);
		$this->page->upDate(new DateTime);
		$this->getUrl($values);
	}

	private function getUrl($values, $iterator = 1) {
		if ($values->url == "/")
			return $values->url;

		if ($values->url == "")
			$url = Strings::webalize($values->name) . ($iterator != 1 ? "-" . $iterator : '' );
		else
			$url = $values->url . ($iterator != 1 ? "-" . $iterator : '' );

		if (count($this->pagesRepository->getAllWhere(['url' => $url])) > 1) {
			$this->getUrl($values, ++$iterator);
		} else {
			$this->page->url($url);
			$this->pagesRepository->save($this->page);
		}
	}

}

No jo, zase se přiznám k tomu menu. Asi bych neměl dávat natvrdo do kódu 4 názvy menu, ale co už. Akutní potřeba převážila :-). Všimni si malé změny oproti formuláři pro editaci uživatele. Tady je metoda pro zpracování odeslaného formuláře přímo ve formuláři, zatímco uživatelé ji mají v presenteru. Můžeš si v rámci cvičení přesunout i u uživatelů metodu pro zpracování k formuláři. Jen jsem chtěl ukázat, že možností je více.

Zajímavá je funkce getUrl. Ta zajišťuje, že nevznikne duplicitní URL.

Pak už jen šablona app\AdminModule\presenters\templates\Pages\edit.latte:

{block content} 
{control editForm}

Přidání stránky

Přidání nové stránky funguje tak, že vždy musím vybrat rodiče a jemu pak přiřadit nově vznikající stránku. Existuje pouze jedna stránka, která nemá žádného rodiče a sama je rodičem všech ostatních stránek. Ve výpisu je u každé existující stránky tlačítko "plus", které spustí formulář pro přidání stránky. Vytvoř tedy soubor app\AdminModule\components\AddPageFormFactory.php.

<?php

namespace App\Forms;

use App\Model\PagesEntity;
use App\Model\PagesRepository;
use Nette\Application\UI\Form;
use Kdyby\BootstrapFormRenderer\BootstrapRenderer;
use Nette\Utils\Strings;
use Nette\Utils\DateTime;

/**
 * Class AddPageFormFactory
 * @package App\Forms
 */
class AddPageFormFactory
{

	/** @var FormFactory */
	private $factory;

	/** @var PagesEntity */
	private $page;

	/** @var PagesRepository */
	private $pagesRepository;

	public function __construct(FormFactory $factory, PagesRepository $pagesRepository) {
		$this->factory = $factory;
		$this->pagesRepository = $pagesRepository;
	}

	/**
	 * @return Form
	 */
	public function create() {
		$form = $this->factory->create();

		$form->addHidden('parent');
		$form->addText('name', 'Název')
				->setAttribute('placeholder', 'Název stránky')
				->setAttribute('class', 'form-control input-sm')
				->setRequired('Zadejte prosím Název');
		$form->addText('url', 'URL')
				->setAttribute('class', 'form-control input-sm')
				->setAttribute('placeholder', 'URL stránky');
		$form->addCheckbox('onHomepage', 'Na úvodní stranu')
				->setAttribute('class', 'bootstrap');
		$form->addCheckboxList('inMenu', 'Položka menu:', [
					'topMenu' => 'Horní menu',
					'footerMenu' => 'Menu v patičce',
					'sideMenu' => 'Postranní menu',
					'otherMenu' => 'Jiné menu',
				])
				->setAttribute('class', 'bootstrap');
		$form->addCheckbox('active', 'Aktivní')
				->setAttribute('class', 'bootstrap');
		$form->addCheckbox('secret', 'Pouze pro přihlášené')
				->setAttribute('class', 'bootstrap');
		$form->addText('menuTitle', 'Název do menu')
				->setAttribute('class', 'form-control input-sm')
				->setAttribute('placeholder', 'Název v menu');
		$form->addText('title', 'Titulek')
				->setAttribute('class', 'form-control input-sm')
				->setAttribute('placeholder', 'Titulek v prohlížeči');
		$form->addText('description', 'Description')
				->setAttribute('class', 'form-control input-sm')
				->setAttribute('placeholder', 'Popisek');
		$form->addText('keywords', 'Keywords')
				->setAttribute('class', 'form-control input-sm')
				->setAttribute('placeholder', 'Klíčová slova');
		$form->addTextArea('perex', 'Perex')
				->setAttribute('class', 'form-control input-sm')
				->setAttribute('placeholder', 'Perex');

		$form->addTextArea('text', 'Text')
				->setAttribute('placeholder', 'Text stránky');
		$form->addTextArea('secretText', 'Text po přihlášení')
				->setAttribute('placeholder', 'Text pro přihlášené uživatele');
		$form->addText('pictureName', 'Obrázek')
				->setAttribute('class', 'form-control input-sm')
				->setAttribute('placeholder', 'Připojený obrázek')
				->setAttribute('onclick', 'openKCFinder(this)');
		$form->addText('pictureDescription', 'Popisek obrázku')
				->setAttribute('class', 'form-control input-sm')
				->setAttribute('placeholder', 'Popisek obrázku');

		$form->addSubmit('save', 'Uložit')
				->setAttribute('class', 'btn btn-primary');
		$form->addSubmit('saveandstay', 'Uložit a zůstat')
				->setAttribute('class', 'btn btn-default');

		$form->setRenderer(new BootstrapRenderer);
		$form->getElementPrototype()->class('form-horizontal');

		$form->onSuccess[] = function (Form $form) {
			$this->formSubmitted($form);
		};
		return $form;
	}

	/**
	 * @param Form $form
	 * @param $form->values
	 */
	private function formSubmitted(Form $form) {
		/** @var PagesEntity $pageParent */
		$pageParent = $this->pagesRepository->get($form->values->parent);
		$this->pagesRepository->begin();

		$this->pagesRepository->prepareBeforeAdd($pageParent);

		$this->page = new PagesEntity;
		$this->page->level($pageParent->level() + 1);
		$this->page->lft($pageParent->rgt());
		$this->page->rgt($pageParent->rgt() + 1);
		$this->page->parent($pageParent->id());
		$this->page->date(new DateTime);
		$this->page->upDate(new DateTime);
		$this->page->name($form->values->name);
		$this->page->inMenu(json_encode($form->values->inMenu));
		$this->page->menuTitle($form->values->menuTitle);
		$this->page->perex($form->values->perex);
		$this->page->text($form->values->text);
		$this->page->pictureName($form->values->pictureName);
		$this->page->pictureDescription($form->values->pictureDescription);
		$this->page->active($form->values->active);
		$this->page->onHomepage($form->values->onHomepage);
		$this->page->secret($form->values->secret);
		$this->page->title($form->values->title);
		$this->page->description($form->values->description);
		$this->page->keywords($form->values->keywords);
		$this->page->secretText($form->values->secretText);
		$this->page->upDate(new DateTime);

		$this->getUrl($form->values);

		$this->pagesRepository->commit();
	}

	private function getUrl($values, $iterator = 1) {
		if ($values->url == "/")
			return $values->url;

		if ($values->url == "")
			$url = Strings::webalize($values->name) . ($iterator != 1 ? "-" . $iterator : '' );
		else
			$url = $values->url . ($iterator != 1 ? "-" . $iterator : '' );

		if (count($this->pagesRepository->getAllWhere(['url' => $url]))) {
			$this->getUrl($values, ++$iterator);
		} else {
			$this->page->url($url);
			$this->pagesRepository->save($this->page);
		}
	}

}

Také tentokrát jsem se rozhodl formulář zpracovávat v továrně formuláře. V presenteru tedy přidej jen funkci pro vytvoření komponenty:

	/**
	 * @return Form
	 */
	protected function createComponentAddForm() {
		$form = $this->addPageFormFactory->create();
		$form->onSuccess[] = function (Form $form) {

			$this->presenter->flashMessage('Stránka byla uložena.', 'success');
			if (isset($form->submitted->name) && $form->submitted->name == 'saveandstay')
				$this->redirect('edit', \Dibi::getInsertId());

			$this->redirect('default');
		};
		return $form;
	}

Normálně není potřeba u akcí s přidáváním řešit metodu renderAdd. Běžně prostě zobrazíme komponentu prázdného formuláře. U stránek to ale neplatí, protože musíme do zobrazeného formuláře dodat ID rodiče nově vznikající stránky. Musíme tedy doplnit poslední funkci v presenteru:

	/**
	 * @param int $id
	 */
	public function renderAdd($id = 1) {
		/** @var Form $form */
		$form = $this['addForm'];
		$row = ['parent' => $id];
		$form->setDefaults($row);
	}

Pak ještě vytvoříme šablonu pro zobrazení formuláře. Vytvoř soubor app\AdminModule\presenters\templates\Pages\add.latte:

{block content}
{control addForm}

Nesmíme zapomenout na definici služeb v config.neon a jejich injestnutí do presenteru. Čili přidej do config.neon řádky:

    - App\Forms\EditPageFormFactory
    - App\Forms\AddPageFormFactory

Nově vzniklé služby použijeme v presenteru:

<?php

namespace App\AdminModule\Presenters;

use App\Model\PagesEntity;
use App\Model\PagesRepository;
use App\Forms\EditPageFormFactory;
use App\Forms\AddPageFormFactory;
use Grido\Grid;
use Nette\Application\UI\Form;
use Nette\Utils\Html;
use PDOException;
use Nette\Utils\DateTime;

/**
 * Class PagesPresenter
 * @package App\AdminModule\Presenters
 */
class PagesPresenter extends BasePresenter
{

	/** @var EditPageFormFactory @inject */
	public $editPageFormFactory;

	/** @var AddPageFormFactory @inject */
	public $addPageFormFactory;

	/** @var PagesRepository @inject */
	public $pagesRepository;

Tak a to je dnes vše. Příště ti ukážu jak udělat všechny ty formuláře hezčí.

Celou aplikaci si opět můžeš stáhnout ve stavu, v jakém bude po dnešní kapitole: kapitola9.zip.

Redakční systém RS::RS Předchozí kapitola

Vícejazyčný web

Redakční systém RS::RS Celý seriál

Vývoj redakčního systému v PHP

Redakční systém RS::RS Následující kapitola

Za formuláře hezčí

 

Komentáře (0)

Přidej svůj komentář

O mně

Jmenuji se Rudolf Svátek. Jsem lektor výpočetní techniky a PHP programátor. Stavím firemní stránky a eshopy. Aby se mi to dělalo pohodlně, vytvořil jsem redakční systém RS::RS, který ti tu nabízím k použití.

Rychlý kontakt na mně

  • Rudolf Svátek
  • Telefon:
    +420 777 828 353
  • Email:
  • Adresa:
    Josefa Hory 1097/5
    736 01 Havířov
    ČR



Tyto stránky používají Cookies. Používáním stránek s tím souhlasíte Další informace