Презентатори

Научете как да създавате презентатори и шаблони в Nette. След като прочетете тази статия, ще знаете.

  • Как работи водещият
  • какво представляват фиксираните параметри
  • Как да визуализирате шаблон

Вече знаем, че презентаторът е клас, който представлява определена страница на уеб приложение, например началната страница, страницата на продукта в онлайн магазин, формата за вход, картата на сайта и т.н. Едно приложение може да има от един до хиляди презентатори. В други рамки те са известни и като контролери.

Обикновено терминът presenter се отнася до наследника на класа Nette\Application\UI\Presenter, който е подходящ за уеб интерфейси. Ще обсъдим този клас в останалата част на тази глава. В общ смисъл презентатор е всеки обект, който реализира интерфейса Nette\Application\IPresenter.

Жизнен цикъл на водещия

Задачата на водещия е да обработи заявката и да върне отговор (това може да бъде HTML страница, изображение, пренасочване и т.н.).

Така че в началото има молба. Това не е самата HTTP заявка, а обектът Nette\Application\Request, в който HTTP заявката е преобразувана от маршрутизатора. Обикновено не се сблъскваме с този обект, тъй като водещият умело делегира обработката на заявката на специални методи, които ще видим след малко.

Предварително въвеждане на жизнения цикъл

На фигурата е показан списък с методи, които се извикват последователно отгоре надолу, ако съществуват. Всички те не са задължителни, можем да имаме напълно празен презентатор без нито един метод и да изградим прост статичен уеб върху него.

__construct()

Конструкторът не е от значение за жизнения цикъл на презентатора, тъй като се извиква в момента на създаване на обекта. Но го споменаваме поради важността му, тъй като се използва за предаване на зависимости.

На водещия не се налага да се грижи за бизнес логиката на приложението, да записва и чете от базата данни, да извършва изчисления и т.н. Това е задача за класовете от слоя, който наричаме модел. Например класът ArticleRepository може да отговаря за зареждането и запазването на статии. За да може презентаторът да я използва, тя се предава чрез имплементация на зависимост:

class ArticlePresenter extends Nette\Application\UI\Presenter
{
	public function __construct(
		private ArticleRepository $articles,
	) {
	}
}

startup()

Веднага след получаване на заявка се извиква методът startup(). Можете да го използвате за инициализиране на свойства, проверка на привилегиите на потребителите и т.н. Необходимо е винаги да се извиква предшественикът parent::startup().

action<Action>(args...)

Подобно на метода render<View>(). Като има предвид, че render<View>() има за цел да подготви данните за конкретен шаблон, който впоследствие се визуализира в action<Action>() заявката се обработва без последващо визуализиране на шаблона. Например данните се обработват, потребителят влиза или излиза от системата и т.н., след което се пренасочват другаде.

Важното е, че action<Action>() се извиква преди render<View>(), така че в него можем евентуално да променим следващия жизнен цикъл, т.е. да променим шаблона за визуализиране и метода render<View>()която ще бъде извикана с помощта на setView('otherView').

Параметрите от заявката се предават на метода. Възможно и препоръчително е да се посочат типове за параметрите, например actionShow(int $id, string $slug = null) – ако параметърът id липсва или ако не е цяло число, презентаторът ще върне грешка 404 и ще прекрати операцията.

handle<Signal>(args...)

Този метод обработва така наречените сигнали, за които ще говорим в главата за Компоненти. Той е предназначен основно за компоненти и обработка на AJAX заявки.

Параметрите се предават на метода, както в action<Action>()включително проверка на типа.

beforeRender()

Методът beforeRender, както подсказва името, се извиква преди всеки метод render<View>(). Той се използва за обща персонализация на шаблона, предаване на променливи за оформление и т.н.

render<View>(args...)

Мястото, където подготвяме шаблона за последващо визуализиране, прехвърляме данни към него и т.н.

Параметрите се предават на метода, както в action<Action>()включително проверка на типа.

public function renderShow(int $id): void
{
	//получаваме данни от модела и ги предаваме в шаблона
	$this->template->article = $this->articles->getById($id);
}

afterRender()

Методът afterRender, както подсказва името, се извиква след всеки метод render<View>(). Той се използва рядко.

shutdown()

Извиква се в края на жизнения цикъл на презентатора.

Добър съвет, преди да продължим. Както виждате, презентаторът може да обработва повече действия/изгледи, т.е. да има повече методи. render<View>(). Но препоръчваме да разработвате презентатори с едно или възможно най-малко действия.

Изпращане на отговор

Обикновено отговорът от главния модул е шаблон за визуализиране на HTML страница, но може да бъде и изпращане на файл, JSON или дори пренасочване към друга страница.

Във всеки момент от жизнения цикъл можем да използваме един от следните методи, за да изпратим отговора и да прекратим работата на презентатора:

  • Пренасочва redirect(), redirectPermanent(), redirectUrl() и forward().
  • error() прекратява работата на водещия поради грешка.
  • sendJson($data) излиза от презентатора и изпраща данни във формат JSON
  • sendTemplate() излиза от презентатора и незабавно визуализира шаблона
  • sendResponse($response) излиза от презентатора и изпраща свой собствен отговор.
  • terminate() прекратява участието на водещия без отговор.

Нящо важно: ако не кажем изрично какъв отговор трябва да изпрати водещият, отговорът е визуализиране на шаблони на HTML. Защо? Ами защото в 99% от случаите искаме да визуализираме шаблон, така че презентаторът приема това поведение по подразбиране и иска да улесни работата ни.

Презентаторът има метод link(), който се използва за създаване на URL връзки към други презентатори. Първият параметър е целевият презентатор и действие, последвани от аргументи, които могат да бъдат предадени като масив:

$url = $this->link('Product:show', $id);

$url = $this->link('Product:show', [$id, 'lang' => 'en']);

В шаблона създаваме връзки към други водещи и действия, както следва:

<a n:href="Product:show $id">страница товара</a>

Просто напишете познатата двойка Presenter:action вместо истинския URL адрес и включете всички параметри. Трикът е n:href, който казва, че този атрибут ще бъде обработен от Latte и ще генерира истинския URL адрес. В Nette изобщо не е необходимо да мислите за URL адреси, а само за презентатори и действия.

За повече информация вижте. Създаване на връзки.

Пренасочване на

Методите redirect() и forward() се използват за пренасочване към друг презентатор и имат синтаксис, много сходен с този на метода link().

Функцията forward() незабавно превключва към новия презентатор без HTTP пренасочване:

$this->forward('Product:show');

Пример за временно пренасочване с HTTP код 302 или 303:

$this->redirect('Product:show', $id);

За да постигнете постоянно пренасочване с HTTP код 301, използвайте:

$this->redirectPermanent('Product:show', $id);

Можете да пренасочите към друг URL адрес извън приложението, като използвате метода redirectUrl():

$this->redirectUrl('https://nette.org');

Пренасочването незабавно прекратява жизнения цикъл на водещия, като хвърля т.нар. изключение за тихо прекратяване Nette\Application\AbortException.

Преди пренасочването може да се изпрати светкавично съобщение, което ще се покаже в шаблона след пренасочването.

Светкавични съобщения

Това са съобщения, които обикновено ви информират за резултата от дадена транзакция. Важна характеристика на флаш съобщенията е, че те са налични в шаблона дори след пренасочване. Дори след като бъдат показани, те ще останат живи още 30 секунди – например в случай, че потребителят неволно опресни страницата – съобщението няма да бъде изгубено.

Просто извикайте метода flashMessage( ) и презентаторът ще се погрижи да предаде съобщението на шаблона. Първият аргумент е текстът на съобщението, а вторият незадължителен аргумент е неговият тип (грешка, предупреждение, информация и т.н.). Методът flashMessage() връща инстанция на флаш съобщението, за да можем да добавим повече информация.

$this->flashMessage('Item was removed.');
$this->redirect(/* ... */);

В шаблона тези съобщения са налични в променливата $flashes като обекти stdClass, които съдържат свойства message (текст на съобщението), type (тип на съобщението) и могат да съдържат вече споменатата информация за потребителя. Извеждаме ги, както следва:

{foreach $flashes as $flash}
	<div class="flash {$flash->type}">{$flash->message}</div>
{/foreach}

Грешка 404 и т.н.

Когато не можем да изпълним дадена заявка, защото например статията, която искаме да покажем, не съществува в базата данни, ще хвърлим грешка 404, като използваме метода error(string $message = null, int $httpCode = 404), който представлява HTTP грешка 404:

public function renderShow(int $id): void
{
	$article = $this->articles->getById($id);
	if (!$article) {
		$this->error();
	}
	// ...
}

Кодът за грешка в HTTP може да бъде подаден като втори параметър, по подразбиране е 404. Методът работи, като хвърля изключение Nette\Application\BadRequestException, след което Application предава управлението на представящия грешката. Задачата му е да покаже страница, която информира за грешката. Предварителният селектор на грешки се задава в конфигурацията на приложението.

Изпращане на JSON

Пример за метод на действие, който изпраща данни във формат JSON и оставя главния модул:

public function actionData(): void
{
	$data = ['hello' => 'nette'];
	$this->sendJson($data);
}

Постоянни параметри

Постоянните параметри се предават автоматично във връзките. Това означава, че не е необходимо да ги посочваме изрично във всеки link() или n:href в шаблона, но те все пак ще бъдат предадени.

Ако приложението ви има няколко езикови версии, текущият език е параметър, който винаги трябва да бъде част от URL адреса. И би било изключително досадно да го споменаваме във всяка връзка. При Nette това не е необходимо. Затова просто отбелязваме параметъра lang като постоянен:

class ProductPresenter extends Nette\Application\UI\Presenter
{
	/** @persistent */
	public $lang;
}

Ако текущата стойност на параметъра lang е 'en', тогава URL адресът, създаден с link() или n:href в шаблона, ще съдържа lang=en. Перфектно!

Можем обаче да добавим и параметъра lang и по този начин да променим стойността му:

<a n:href="Product:show $id, lang: en">подробности на английском</a>

Или пък можем да го изтрием, като му зададем стойност null:

<a n:href="Product:show $id, lang: null">нажмите здесь</a>

Една постоянна променлива трябва да бъде декларирана като публична. Можем да зададем и стойност по подразбиране. Ако параметърът има същата стойност като тази по подразбиране, той няма да бъде включен в URL адреса.

Постоянството отразява йерархията на класовете на презентаторите, така че параметър, дефиниран в конкретен презентатор или черта, се предава автоматично на всеки презентатор, който наследява от него или използва същата черта.

В PHP 8 можете също така да използвате атрибути, за да маркирате постоянни параметри:

use Nette\Application\Attributes\Persistent;

class ProductPresenter extends Nette\Application\UI\Presenter
{
	#[Persistent]
	public $lang;
}

Интерактивни компоненти

Презентаторите имат вградена система от компоненти. Компонентите са отделни единици за многократна употреба, които поставяме в презентаторите. Това могат да бъдат формуляри, решетки за данни, менюта, изобщо всичко, което има смисъл да се използва многократно.

Как се поставят и впоследствие използват компонентите в презентатора? Това е обяснено в глава Компоненти. Дори ще разберете какво общо имат те с Холивуд.

Къде мога да купя някои компоненти? На страницата Componette можете да намерите някои компоненти с отворен код и други добавки за Nette, които са създадени и се разпространяват от общността на рамката Nette.

Навлезте по-дълбоко в

Това, което показахме досега в тази глава, вероятно ще бъде достатъчно. Следващите редове са за тези, които се интересуват от презентаторите обстойно и искат да знаят всичко.

Изисквания и параметри

Заявката, която се обработва от водещия, е обект Nette\Application\Request и се връща от метода на водещия getRequest(). Той включва масив от параметри, всеки от които принадлежи или на някой компонент, или директно на водещия (който всъщност също е компонент, макар и специален). По този начин Nette преразпределя параметрите и ги прехвърля между отделните компоненти (и водещия) чрез извикване на метода loadState(array $params), който е описан по-подробно в главата Компоненти. Параметрите могат да се извличат чрез метода getParameters(): array, а поотделно – чрез getParameter($name). Стойностите на параметрите са низове или масиви от низове, предимно необработени данни, получени директно от URL.

Запазване и възстановяване на заявка

Можете да запазите текущата заявка в сесия или да я възстановите от сесия и да позволите на водещия да я изпълни отново. Това е полезно, например когато потребителят попълни формуляр и срокът му на влизане изтече. За да избегнем загубата на данни, преди да пренасочим към страницата за регистрация, запазваме текущата заявка в сесията с функцията $reqId = $this->storeRequest(), която връща идентификатора като кратък низ и го предава като параметър на водещия за регистрация.

След като влезем в системата, извикваме метода $this->restoreRequest($reqId), който извлича заявката от сесията и я препраща към нея. Методът проверява дали заявката е създадена от същия потребител, който в момента е влязъл в системата. Ако е влязъл друг потребител или ключът е невалиден, не се прави нищо и програмата продължава.

Вижте глава Как да се върнете на предишната страница.

Канонизация

Презентаторите имат една наистина чудесна функция, която подобрява SEO. Те автоматично предотвратяват съществуването на дублирано съдържание на различни URL адреси. Ако няколко URL адреса водят до определена дестинация, например /index и /index?page=1, рамката определя един от тях за основен (каноничен) URL адрес и пренасочва останалите към него, като използва код 301 HTTP. Това не позволява на търсачките да индексират страниците два пъти и да влошат класирането им.

Този процес се нарича канонизиране. Каноничният URL адрес е URL адрес, генериран от маршрут, обикновено първият съвпадащ маршрут в колекцията.

Канонизацията е разрешена по подразбиране и може да бъде забранена с помощта на $this->autoCanonicalize = false.

Пренасочването не се извършва при заявка AJAX или POST, тъй като това ще доведе до загуба на данни или няма да има полза за SEO.

Можете също така да извикате канонизацията ръчно с метода canonicalize(), който, както и методът link(), приема като аргументи водещия, действията и параметрите. Тя създава връзка и я сравнява с текущия URL адрес. Ако те са различни, се пренасочва към генерираната връзка.

public function actionShow(int $id, string $slug = null): void
{
	$realSlug = $this->facade->getSlugForId($id);
	// пренасочва, ако $slug е различен от $realSlug
	$this->canonicalize('Product:show', [$id, $realSlug]);
}

Събития

В допълнение към методите startup(), beforeRender() и shutdown(), които се извикват като част от жизнения цикъл на презентатора, можете да дефинирате други функции, които да се извикват автоматично. Презентаторът дефинира така наречените събития, а вие добавяте техните обработчици към масивите $onStartup, $onRender и $onShutdown.

class ArticlePresenter extends Nette\Application\UI\Presenter
{
	public function __construct()
	{
		$this->onStartup[] = function () {
			// ...
		};
	}
}

Обработващите в масива $onStartup се извикват непосредствено преди метода startup(), след това $onRender между beforeRender() и render<View>() и накрая $onShutdown точно преди shutdown().

Отговори

Отговорът, върнат от водещия, е обект, който реализира интерфейса Nette\Application\Response. Има няколко готови отговора:

Отговорите се изпращат по метода sendResponse():

use Nette\Application\Responses;

// Обикновен текст
$this->sendResponse(new Responses\TextResponse('Hello Nette!'));

// Изпраща файл
$this->sendResponse(new Responses\FileResponse(__DIR__ . '/invoice.pdf', 'Invoice13.pdf'));

// Изпраща обратна връзка
$callback = function (Nette\Http\IRequest $httpRequest, Nette\Http\IResponse $httpResponse) {
	if ($httpResponse->getHeader('Content-Type') === 'text/html') {
		echo '<h1>Hello</h1>';
	}
};
$this->sendResponse(new Responses\CallbackResponse($callback));

Допълнително четене

версия: 4.0