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

Научете как да създавате презентатори и шаблони в 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, ако текущият метод на заявка е POST):

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

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

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

Можете да пренасочите към друг URL адрес извън приложението, като използвате метода redirectUrl(). Като втори параметър може да се посочи HTTP кодът, като по подразбиране той е 302 (или 303, ако текущият метод на заявка е POST):

$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);
}

Параметри на заявката

Презентаторът, както и всеки компонент, получава своите параметри от HTTP заявката. Стойностите им могат да бъдат извлечени с помощта на метода getParameter($name) или getParameters(). Стойностите са низове или масиви от низове, по същество сурови данни, получени директно от URL адреса.

За по-голямо удобство препоръчваме да направите параметрите достъпни чрез свойства. Просто ги анотирайте с #[Parameter] атрибут:

use Nette\Application\Attributes\Parameter;  // този ред е важен

class HomePresenter extends Nette\Application\UI\Presenter
{
	#[Parameter]
	public string $theme; // трябва да е публичен
}

За свойствата предлагаме да посочите типа данни (например string). След това Nette автоматично ще определи стойността въз основа на него. Стойностите на параметрите също могат да бъдат валидирани.

Когато създавате връзка, можете директно да зададете стойност за параметрите:

<a n:href="Home:default theme: dark">click</a>

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

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

Пример за използване? Имате многоезично приложение. Действителният език е параметър, който трябва да бъде част от URL адреса по всяко време. Но би било изключително досадно да го включвате във всяка връзка. Затова го правите постоянен параметър с име lang и той ще се пренася сам. Страхотно!

Създаването на постоянен параметър е изключително лесно в Nette. Просто създайте публично свойство и го маркирайте с атрибута: (преди се използваше /** @persistent */ )

use Nette\Application\Attributes\Persistent; // този ред е важен

class ProductPresenter extends Nette\Application\UI\Presenter
{
	#[Persistent]
	public string $lang; // трябва да бъдат публични
}

Ако $this->lang има стойност като 'en', то връзките, създадени с помощта на link() или n:href, ще съдържат и параметъра lang=en. И когато върху връзката се щракне, тя отново ще бъде $this->lang = 'en'.

За свойствата препоръчваме да включите типа на данните (например string), като можете да включите и стойност по подразбиране. Стойностите на параметрите могат да бъдат валидирани.

Постоянните параметри се предават между всички действия на даден презентатор по подразбиране. За да ги предадете между няколко водещи, трябва да ги дефинирате или:

  • в общ предшественик, от който презентаторите наследяват
  • в чертата, която презентаторите използват:
trait LanguageAware
{
	#[Persistent]
	public string $lang;
}

class ProductPresenter extends Nette\Application\UI\Presenter
{
	use LanguageAware;
}

Можете да променяте стойността на постоянен параметър, когато създавате връзка:

<a n:href="Product:show $id, lang: cs">detail in Czech</a>

Или може да бъде ресетнат, т.е. да бъде премахнат от URL адреса. След това той ще приеме стойността си по подразбиране:

<a n:href="Product:show $id, lang: null">click</a>

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

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

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

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

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

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

Утвърждаване на параметрите

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

Никога не се доверявайте сляпо на параметрите, тъй като те лесно могат да бъдат пренаписани от потребителя в URL адреса. Например, така проверяваме дали $this->lang е сред поддържаните езици. Добър начин да направите това е да презапишете метода loadState(), споменат по-горе:

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

	public function loadState(array $params): void
	{
		parent::loadState($params); // тук се задава $this->lang
		// следва проверката на потребителската стойност:
		if (!in_array($this->lang, ['en', 'cs'])) {
			$this->error();
		}
	}
}

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

Заявката, която се обработва от водещия, е обект Nette\Application\Request и се връща от метода на водещия getRequest().

Можете да запазите текущата заявка в сесия или да я възстановите от сесия и да позволите на водещия да я изпълни отново. Това е полезно, например, когато потребител попълни формуляр и срокът на неговото влизане в системата изтече. За да не загубим данни, преди да пренасочим към страницата за вписване, запазваме текущата заявка в сесия с помощта на $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));

Ограничаване на достъпа с помощта на #[Requires]

В #[Requires] предоставя разширени опции за ограничаване на достъпа до презентаторите и техните методи. Той може да се използва за определяне на HTTP методи, изискване на AJAX заявки, ограничаване на достъпа до същия произход и ограничаване на достъпа само до препращане. Атрибутът може да се прилага към класове на презентатори, както и към отделни методи, като например action<Action>(), render<View>(), handle<Signal>(), и createComponent<Name>().

Можете да зададете тези ограничения:

  • на HTTP методите: #[Requires(methods: ['GET', 'POST'])]
  • изискващи AJAX заявка: #[Requires(ajax: true)]
  • достъп само от един и същ произход: #[Requires(sameOrigin: true)]
  • достъп само чрез препращане: #[Requires(forward: true)]
  • ограничения за конкретни действия: #[Requires(actions: 'default')]

За подробности вижте Как да използвате Requires атрибут.

Проверка на метода HTTP

В Nette презентаторите автоматично проверяват HTTP метода на всяка входяща заявка главно от съображения за сигурност. По подразбиране са разрешени методите GET, POST, HEAD, PUT, DELETE, PATCH.

Ако искате да разрешите допълнителни методи, като например OPTIONS, можете да използвате #[Requires] атрибут (от Nette Application v3.2):

#[Requires(methods: ['GET', 'POST', 'HEAD', 'PUT', 'DELETE', 'PATCH', 'OPTIONS'])]
class MyPresenter extends Nette\Application\UI\Presenter
{
}

Във версия 3.1 проверката се извършва в checkHttpMethod(), който проверява дали методът, посочен в заявката, е включен в масива $presenter->allowedMethods. Добавете метод по следния начин:

class MyPresenter extends Nette\Application\UI\Presenter
{
    protected function checkHttpMethod(): void
    {
        $this->allowedMethods[] = 'OPTIONS';
        parent::checkHttpMethod();
    }
}

Изключително важно е да се подчертае, че ако разрешите метода OPTIONS, трябва също така да го обработвате правилно в рамките на вашия презентатор. Този метод често се използва като така наречената preflight заявка, която браузърите автоматично изпращат преди действителната заявка, когато е необходимо да се определи дали заявката е разрешена от гледна точка на политиката CORS (Cross-Origin Resource Sharing). Ако разрешите този метод, но не приложите подходящ отговор, това може да доведе до несъответствия и потенциални проблеми със сигурността.

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

версия: 4.0