Menu, kontaktní formulář a další komponenty

Rudolf Svátek 2016-07-27 15:12

​Aplikaci si můžeš stáhnout: kapitola18.zip

Redakční systém musí umět spousty věcí. Do těch základních patří generování menu, vytvoření a ošetření kontaktního formuláře, zobrazení novinek a tak dále. Mysli na to, že svůj redakční systém třeba někomu prodáš a ten by měl být schopen ho používat už bez tvých zásahů. Tedy alespoň ve standardních situacích, jako dostat nějakou stránku do menu, nebo na vybrané stránce zobrazit kontaktní formulář. Takže na každou takovou věc si připravíme komponentu, kterou pak vložíme buď přímo do šablony (to třeba v případě menu, které se asi jinde zobrazovat nemusí), nebo umožníme vložit komponentu přímo do textu stránky.

Každá komponenta si nese i vlastní šablonu a budeme je ukládat do složky app\FrontModule\components.

Menu

Začneme tvorbou menu. Vždy, když v tomto seriálu narazím na téma "menu", trochu se trápím, že moje řešení není úplně programátorsky čisté. Mělo by to být tak, že by existovala tabulka, ve které bych si založil tolik různých menu, kolik bych chtěl. Pak další tabulka, která by definovala různé položky menu. Každá taková položka by věděla ke kterému menu patří a také by znala svůj cíl - stránku v redakčním systému, odkaz na výpis novinek, externí URL adresu apod.

Ale zjistil jsem, že uživatelé, kteří by to pak v administraci měli vytvářet, se raději obrací na mne, abych jim to udělal. Proto jsem sáhl k jednoduššímu řešení - stránky mají příznak do kterého ze 4 menu patří. Menu se pak vygeneruje pouze ze stránek a uživatel nemusí nic složitě budovat. Nevýhodou je špatná rozšiřitelnost, když uživatelé potřebují více než 4 různá menu, nebo pokud potřebují mít v menu odkaz jinam, než na stránku v redakčním systému.

Zatím jsem se s podobným požadavkem nesetkal u žádného svého klienta, takže menu prostě zatím lépe neřeším :-)

Takže vytvoř soubor app\FrontModule\components\MenuControl.php. Začátek je:

<?php

namespace App\FrontModule\Presenters;

use App\Model\PagesRepository;
use Nette\Application\UI\Control;

class MenuControl extends Control
{

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

	/** @var string */
	protected $menuType;

	/**
	 * MenuControl constructor.
	 * @param PagesRepository $pagesRepository
	 * @param string $menuType
	 */
	public function __construct(PagesRepository $pagesRepository, string $menuType) {
		parent::__construct();
		$this->pagesRepository = $pagesRepository;
		$this->menuType = $menuType;
	}

Dědí tedy od třídy Control, která je součástí Nette. Budeme taky potřebovat repositář stránek, abychom dokázali vyhledat ty, které patří do menu. V kontroktoru mám 2 argumenty. Ten druhý ponese název menu, které chci naplnit. Třeba "topMenu", "footerMenu" apod.

Komponenty by měly mít metodu render(), ve které si připravíme data pro zobrazení v šabloně. Taky můžeme říct, kde se šablona nachází. Metodu tedy doplníme:

	public function render() {
		$template = $this->getTemplate();
		if (file_exists(__DIR__ . '/' . $this->menuType . '.latte'))
			$template->setFile(__DIR__ . '/' . $this->menuType . '.latte');
		else
			$template->setFile(__DIR__ . '/topMenu.latte');
		$pagesInMenu = $this->pagesRepository->getPagesInMenu($this->menuType);
		$template->menuItems = $this->convertAdjacencyListToTree(1, $pagesInMenu, 'id', 'parent', 'children');
		$template->render();
	}

Asi pochopíš, co se tam děje - najdeme soubor s šablonou. Pokud se to nepovede, bude to topMenu.latte. Najdeme všechny stránky, které do daného menu patří. Výsledek proženeme funkcí convertAdjacencyListToTree, která sestaví strom stránek tak, jak by měl být v menu.

Funkce convertAdjacencyListToTree je rekurzivní:

	/**
	 * @param $intParentId
	 * @param $arrRows
	 * @param $strIdField
	 * @param $strParentsIdField
	 * @param $strNameResolution
	 * @return array
	 */
	private function convertAdjacencyListToTree($intParentId, &$arrRows, $strIdField, 
			$strParentsIdField, $strNameResolution) {

		$arrChildren = [];
		for ($i = 0; $i < count($arrRows); $i++)
			if ($intParentId === $arrRows[$i][$strParentsIdField])
				$arrChildren = array_merge($arrChildren, array_splice($arrRows, $i--, 1));

		$intChildren = count($arrChildren);
		if ($intChildren != 0)
			for ($i = 0; $i < $intChildren; $i++)
				$arrChildren[$i][$strNameResolution] = $this->convertAdjacencyListToTree(
						$arrChildren[$i][$strIdField], $arrRows, $strIdField, $strParentsIdField, $strNameResolution);

		return $arrChildren;
	}

Tady nevím jak to jednoduše popsat, takže zkus si vypisovat výsledek a uvidíš co a jak.

Šablona "topMenu.latte" pak už jen stránky vypisuje:

<ul class="nav navbar-nav nav-pills ddmenu">
{block #menu}
		{foreach $menuItems as $link}
			<li n:class="count($link->children) ? dropdown">
				{if count($link->children)}
					<a href="{plink Pages:view $link->id}" role="button" data-toggle="dropdown" class="dropdown-toggle">{$link->menuTitle} <span class="caret"></span></a>
					<ul class="dropdown-menu">
					{include #menu, menuItems => $link->children}
					</ul>
				{else}
					<a href="{plink Pages:view $link->id}">{$link->menuTitle}</a>
				{/if}
			</li>
		{/foreach}
{/block}
</ul>

Na šabloně ja zajímavé, že také používá rekurzi - všimni si, že na řádku 2 pojmenovávám blok, který v případě, že axistují podstránky, vkládám znovu na řádku 8.

Tím mám hotovo a teď už jen do hlavní šablony vložíme celou komponentu tak, že nahradíme HTML kód:

<ul class="nav navbar-nav nav-pills ddmenu sm">
	<li>
		<a href="#">Domů</a>
	</li>
	<li>
		<a href="#">O nás</a>
	</li>
	<li>
		<a href="#">Kontakty</a>
	</li>
</ul>

za malý kousek:

{control topMenu}

Ještě to ale nezafunguje, protože chceme budovat odkaz na zobrazení stránky, ale presenter a šablona, které by to uměly, ještě neexistují. Potřebujeme tedy PagesPresenter.php. Bude hodně podobný HomepagePresenteru:

<?php

namespace App\FrontModule\Presenters;

class PagesPresenter extends BasePresenter
{

	public function startup() {
		parent::startup();
	}

	/**
	 * @param $id
	 */
	public function actionView($id) {
		$page = $this->pagesRepository->get($id);
		if($page->secret() == 1 && !$this->user->isLoggedIn()) {
			$this->flashMessage('Ke stránce ' . (string)$page->name() . ' nemáte oprávnění', 'danger');
			$this->redirect('Homepage:default');
		}

		$this->getTemplate()->page = $page;
		$this->getTemplate()->title = ($page->title() > '' ? $page->title() : $page->name());
		$this->getTemplate()->description = $page->description() > '' ? $page->description() : $page->text();
		$this->getTemplate()->keywords = $page->keywords() > '' ? $page->keywords() : '';
	}

}

Jediné, čím se liší, je test, zda je uživatel přihlášen, pokud je zobrazovaná stránka nepřístupná nepřihlášeným uživatelům. Šablona app\FrontModules\presenters\templates\Pages\view.latte je také jednoduchá:

{block content}
	<div class="divPanel notop nobottom">
		<div class="row-fluid">
			<div class="span12">
				<div id="contentInnerSeparator"></div>
			</div>
		</div>
	</div>
	<div class="contentArea">
		<div class="divPanel notop page-content">
			<div class="breadcrumbs">
			</div>
			<div class="row-fluid">
				<!--Edit Main Content Area here-->
				<div class="span12" id="divMain">
					{$page->text()|noescape}
					{if $user->isLoggedIn() && $page->secretText() > ''}
						{$page->secretText()|noescape}
					{/if}
				</div>
				<!--End Main Content-->
			</div>
			<div id="footerInnerSeparator"></div>
		</div>
	</div>
{/block}

Prostě si to sestav jak chceš a v šabloně můžeš použít proměnné z presenteru. Ale ještě nejsme na úplném konci. Někde musíme požádat o vytvoření komponenty menu. Jelikož menu chceme zobrazovat asi vždy, nabízí se BasePresenter. Vlož do něj metodu:

	/**
	 * @return MenuControl
	 */
	protected function createComponentTopMenu() {
		$menu = new MenuControl($this->pagesRepository, 'topMenu');
		return $menu;
	}

Kontaktní formulář

Narozdíl od menu je dobré umět kontaktní formulář zobrazit na různých místech. Bylo by snadné vytvořit zase komponentu a umístit ji do šablony. A vlastně to uděláme tak, aby to prostě šlo. Ale navíc naučíme redakční systém milou věcičku: když se bude vykreslovat obsah stránky, čili bude pracovat PagesPresenter, budeme chtít, aby v textu hledal vložený kód komponenty. Pokud takový kód najde, tak budeme chtít, aby se vykreslila komponenta se svou šablonou, proměnnými a funkčností. No, ale nejdříve vytvoříme samotnou komponentu kontaktního formuláře:

<?php

namespace App\FrontModule\Controls;

use App\Model\PagesRepository;
use App\Forms\FormFactory;
use Nette\Application\UI;
use Kdyby\BootstrapFormRenderer\BootstrapRenderer;
use Nette\Mail\Message;
use Nette\Mail\SmtpMailer;

class ContactControl extends UI\Control
{

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

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

	/**
	 * @param PagesRepository $pagesRepository
	 */
	public function __construct(PagesRepository $pagesRepository, FormFactory $factory) {
		parent::__construct();
		$this->pagesRepository = $pagesRepository;
		$this->factory = $factory;
	}

	public function render() {
		$this->getTemplate()->setFile(__DIR__ . '/ContactControl.latte');
		$this->getTemplate()->render();
	}

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

		$form->addText('name', 'Vaše jméno:')
				->addRule(UI\Form::FILLED, 'Zadejte jméno')
				->setAttribute('class', 'form-control input-sm');
		$form->addText('email', 'Vaše e-mailová adresa:')
				->addRule(UI\Form::EMAIL, 'Email nemá správný formát')
				->setAttribute('class', 'form-control input-sm');
		$form->addText('subject', 'Předmět:')
				->addRule(UI\Form::FILLED, 'Zadejte předmět')
				->setAttribute('class', 'form-control input-sm');

		$form->addTextArea('message', 'Zpráva:')
				->addRule(UI\Form::FILLED, 'Zadejte zprávu')
				->setAttribute('class', 'form-control input-sm');

		$form->addSubmit('process', 'Odeslat')
				->setAttribute('class', 'btn btn-primary');

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

		$form->onSuccess[] = callback($this, 'processForm');
		return $form;
	}

	/**
	 * @param UI\Form $form
	 */
	public function processForm(UI\Form $form) {
		$values = $form->getValues();
		$template = $this->createTemplate();
		$mail = new Message;

		$template->setFile(__DIR__ . '/../presenters/templates/Emails/contactForm.latte');

		$mail->addTo('admin@localhost.loc')
				->setFrom($values['email']);

		$template->title = 'Zpráva z kontaktního formuláře';
		$template->values = $values;

		$mail->setHtmlBody($template);
		$this->mailer = $this->setMailer();
		$this->mailer->send($mail);
		$this->flashMessage('Zpráva byla odeslána');
		$this->presenter->redirect('Homepage:');
	}

	/**
	 * @return SmtpMailer
	 */
	private function setMailer() {
		/** @var SmtpMailer mailer */
		$mailer = new SmtpMailer([
			'host' => 'smtpHost',
			'username' => 'smtpUsername',
			'password' => 'smtpPassword',
			'secure' => 'smtpSecure',
		]);
		return $mailer;
	}

}

Tohle by mělo být jasné - v konstruktoru zajistím závislosti, metoda render() jen řekne, kde leží šablona, pak je tam definice formuláře a jeho zpracování a nakonec nastavení SMTP maileru. Pokud čteš pozorně každou kapitolu, tak víš, že ten jsme už někde nastavovali. Příště si tedy ukážeme, jak využít, že v administraci si nastavujeme různé věci jako název stránek, email majitele, nebo i ten SMTP server.

Šablona může být fakt jen jednoduché uvedení jednoho řádku:

{control contactForm}

A nebo pokud chceš mít úplnou kontrolu nad HTML kódem, vykreslíš to ručně:

<form n:name="contactForm" class="form-horizontal" method="post" novalidate="">
	<div class="control-group required" id="frm-contactForm-name-pair">
		<label n:name="name" class="required control-label">Vaše jméno:</label>
		<div class="controls">
			<input n:name="name" type="text" class="form-control input-sm">
		</div>
	</div>
	<div class="control-group" id="frm-contactForm-email-pair">
		<label n:name="email" class="control-label">Vaše e-mailová adresa:</label>
		<div class="controls">
			<input n:name="email" class="form-control input-sm">
		</div>
	</div>
	<div class="control-group required" id="frm-contactForm-subject-pair">
		<label n:name="subject" class="required control-label">Předmět:</label>
		<div class="controls">
			<input n:name="subject" class="form-control input-sm">
		</div>
	</div>
	<div class="control-group required" id="frm-contactForm-message-pair">
		<label n:name="message" class="required control-label">Zpráva:</label>
		<div class="controls">
			<textarea n:name="message" class="form-control input-sm"></textarea>
		</div>
	</div>
	<div class="form-actions">
		<input n:name="process" class="btn btn-primary btn">
	</div>
</form>

Zase tě čeká BasePresenter, kam přidáš metodu pro vytvoření komponenty:

	/**
	 * @return ContactControl
	 */
	protected function createComponentContactForm() {
		$control = new ContactControl($this->pagesRepository, $this->formFactory);
		return $control;
	}

Zobrazení kontaktního formuláře v šabloně je snadné. Dejme tomu, že ho chci zobrazit v patičce. Potom vložím do @layout.latte na správné místo:

{control contactForm}

Zobrazení komponenty v textu stránky

Jak ale zařídit, aby uživatel v editaci stránek určil, že na této konkrétní stránce se má formulář také ukázat? Tušíš, že budeme upravovat PagesPresenter. Do metody startup přidáme definici filtru latte:

	public function startup() {
		parent::startup();
		$this->getTemplate()->addFilter('components', function ($text) {
			$presenter = $this;

			return preg_replace_callback('~\#(.*)\#~', function ($matches) use ($presenter) {
				$array = explode(", ", $matches[1]);
				$component = $array[0];
				ob_start();
				$component = $presenter->getComponent($component);
				unset($array[0]);
				if (count($array))
					$component->render($array);
				else
					$component->render();
				return ob_get_clean();
			}, $text);
		});
	}

!!POZOR!! - v to výpisu nahoře vidíš v regulárním výrazu jen jeden hashtag na začátku a jeden na konci. Uprav si to tak, abys tam měl 2. Já to sem dát nemůžu, protože podmínkou samozřejmě je, že zvolíš takové znaky na začátku i na konci, které se nikdy nevyskytnou v textu. Proto kdybych to sem napsal s dvěma hashtagy, při zobrazení této kapitoly by můj systém hledal komponentu s názvem (.*).

Tohle už je celkem masakr, takže lehce naznačím vysvětlení.

  • funkce addFilter umí přidat nějaký modifikátor, který vypisuje původní hodnotu nějak naformátovanou, nebo přímo upravenou. Latte má takových filtrů samo o sobě spousty - formátování data, měny, odstranění diakritiky apod.
  • hledáme regulárním výrazem v textu něco, co začíná dvěma hashtagy a je jimi i ukončeno. To, co je uvnitř takových dvojitých hashtagů, budeme považovat za název komponenty.
  • pokud jsme něco takového našli, požádáme presenter, aby nám komponentu předal.
  • když presenter uspěje a komponentu najde, tak komponentu vykreslíme metodou render()

Takže když máme filtr vytvořený, můžeme ho použít v šabloně při výpisu textů. Teď tedy otevři šablonu pro zobrazení stránky app\FrontModule\presenters\templates\Pages\view.latte. Změň řádek s výpisem textu na:

{$page->text()|noescape|components}

Přibylo tam to "|components". Teď (pokud ji ještě nemáš) založ v administraci stránku "Kontakty", nastav jí, aby byla aktivní a zařazena v Top Menu. Do textu vlož co chceš, ale určitě přidej ty dvojité hashtagy:

Komponenta v textuJakmile uložíš a pak zobrazíš stránku, uvidíš přímu uvnitř stránky ten formulář. Takhle to funguje s každou další komponentou, kterou vytvoříš.

​Aplikaci si můžeš stáhnout: kapitola18.zip

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

Úvodní stránka

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

Komentáře k článkům

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