Презентери
Ще се запознаем с това как се пишат презентери и шаблони в Nette. След като прочетете, ще знаете:
- как работи презентерът
- какво са персистентните параметри
- как се рендират шаблони
Вече знаем, че презентерът е клас, който представлява някаква конкретна страница на уеб приложение, напр. начална страница; продукт в електронен магазин; формуляр за вход; sitemap feed и т.н. Приложението може да има от един до хиляди презентери. В други фреймуърци те се наричат и контролери.
Обикновено под понятието презентер се разбира наследник на клас Nette\Application\UI\Presenter, който е подходящ за генериране на уеб интерфейси и на който ще се посветим в останалата част от тази глава. В общ смисъл презентерът е всеки обект, имплементиращ интерфейса Nette\Application\IPresenter.
Жизнен цикъл на презентера
Задачата на презентера е да обработи заявка и да върне отговор (който може да бъде HTML страница, изображение, пренасочване и т.н.).
Следователно в началото му се предава заявка. Това не е директно HTTP заявка, а обект Nette\Application\Request, в който HTTP заявката е била трансформирана с помощта на рутера. С този обект обикновено не влизаме в контакт, тъй като презентерът умно делегира обработката на заявката на други методи, които сега ще покажем.
Изображението представлява списък с методи, които се извикват последователно отгоре надолу, ако съществуват. Никой от тях не е задължителен, можем да имаме напълно празен презентер без нито един метод и да изградим върху него прост статичен уебсайт.
__construct()
Конструкторът не принадлежи съвсем към жизнения цикъл на презентера, защото се извиква в момента на създаване на обекта. Но го споменаваме поради важността му. Конструкторът (заедно с метода inject) служи за предаване на зависимости.
Презентерът не трябва да се занимава с бизнес логиката на
приложението, да записва и чете от база данни, да извършва изчисления и
т.н. За това са класовете от слоя, който наричаме модел. Например класът
ArticleRepository
може да отговаря за зареждането и съхраняването на
статии. За да може презентерът да работи с него, той си го изисква чрез dependency injection:
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('jineView')
.
На метода се предават параметри от заявката. Възможно е и се
препоръчва да се посочат типове на параметрите, напр.
actionShow(int $id, ?string $slug = null)
– ако параметърът id
липсва или
ако не е integer, презентерът ще върне грешка 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()
Извиква се в края на жизнения цикъл на презентера.
Добър съвет, преди да продължим. Презентерът, както се вижда, може
да обслужва повече действия/view, т.е. да има повече методи
render<View>()
. Но препоръчваме да проектирате презентери с едно
или възможно най-малко действия.
Изпращане на отговор
Отговорът на презентера обикновено е рендиране на шаблон с HTML страница, но може да бъде и изпращане на файл, JSON или например пренасочване към друга страница.
По всяко време на жизнения цикъл можем с някой от следните методи да изпратим отговор и същевременно да прекратим презентера:
redirect()
,redirectPermanent()
,redirectUrl()
иforward()
пренасочватerror()
прекратява презентера поради грешкаsendJson($data)
прекратява презентера и изпраща данни във формат JSONsendTemplate()
прекратява презентера и веднага рендира шаблонsendResponse($response)
прекратява презентера и изпраща собствен отговорterminate()
прекратява презентера без отговор
Ако не извикате никой от тези методи, презентерът автоматично ще пристъпи към рендиране на шаблона. Защо? Защото в 99% от случаите искаме да рендираме шаблон, следователно презентерът приема това поведение като подразбиращо се и иска да ни улесни работата.
Създаване на връзки
Презентерът разполага с метод link()
, с помощта на който могат да
се създават URL връзки към други презентери. Първият параметър е
целевият презентер и действие, следват предаваните аргументи, които
могат да бъдат посочени като масив:
$url = $this->link('Product:show', $id);
$url = $this->link('Product:show', [$id, 'lang' => 'cs']);
В шаблона се създават връзки към други презентери и действия по следния начин:
<a n:href="Product:show $id">детайл на продукта</a>
Просто вместо реален URL напишете познатата двойка Presenter:action
и
посочете евентуални параметри. Трикът е в n:href
, което казва, че
този атрибут ще бъде обработен от Latte и ще генерира реален URL. В Nette така
изобщо не е необходимо да мислите за URL, само за презентери и
действия.
Повече информация ще намерите в главата Създаване на 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
.
Преди пренасочване може да се изпрати flash съобщение, т.е. съобщения, които ще бъдат показани в шаблона след пренасочването.
Flash съобщения
Това са съобщения, обикновено информиращи за резултата от някаква операция. Важна характеристика на flash съобщенията е, че те са достъпни в шаблона и след пренасочване. Дори след показване остават активни още 30 секунди – например в случай, че поради грешка при прехвърлянето потребителят обнови страницата – съобщението няма да изчезне веднага.
Достатъчно е да извикате метода flashMessage() и за
предаването в шаблона ще се погрижи презентерът. Първият параметър е
текстът на съобщението, а незадължителният втори параметър е неговият
тип (error, warning, info и др.). Методът flashMessage()
връща инстанция на flash
съобщението, към което могат да се добавят допълнителни информации.
$this->flashMessage('Елементът беше изтрит.');
$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)
.
public function renderShow(int $id): void
{
$article = $this->articles->getById($id);
if (!$article) {
$this->error();
}
// ...
}
HTTP кодът на грешката може да се предаде като втори параметър, по
подразбиране е 404. Методът работи така, че хвърля изключение
Nette\Application\BadRequestException
, след което Application
предава
управлението на error-presenter. Което е презентер, чиято задача е да покаже
страница, информираща за възникналата грешка. Настройката на error-preseter
се извършва в конфигурацията
application.
Изпращане на JSON
Пример за action-метод, който изпраща данни във формат 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; // трябва да е public
}
При свойството препоръчваме да посочите и типа данни (напр.
string
) и Nette според него автоматично претипира стойността.
Стойностите на параметрите могат също да бъдат валидирани.
При създаване на връзка може директно да се зададе стойност на параметрите:
<a n:href="Home:default theme: dark">кликни</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; // трябва да е public
}
Ако $this->lang
има стойност например 'en'
, то и връзките,
създадени с помощта на link()
или n:href
, ще съдържат
параметъра lang=en
. И след кликване върху връзката отново ще бъде
$this->lang = 'en'
.
При свойството препоръчваме да посочите и типа данни (напр.
string
) и можете да посочите и стойност по подразбиране.
Стойностите на параметрите могат да бъдат валидирани.
Персистентните параметри стандартно се пренасят между всички действия на дадения презентер. За да се пренасят и между няколко презентера, е необходимо да се дефинират или:
- в общ родител, от който презентерите наследяват
- в trait, който презентерите използват:
trait LanguageAware
{
#[Persistent]
public string $lang;
}
class ProductPresenter extends Nette\Application\UI\Presenter
{
use LanguageAware;
}
При създаване на връзка може да се промени стойността на персистентния параметър:
<a n:href="Product:show $id, lang: cs">детайл на български</a>
Или може да бъде ресетнат, т.е. премахнат от URL. Тогава ще приеме своята стойност по подразбиране:
<a n:href="Product:show $id, lang: null">кликни</a>
Интерактивни компоненти
Презентерите имат вградена компонентна система. Компонентите са самостоятелни цялости за многократна употреба, които вмъкваме в презентерите. Могат да бъдат формуляри, datagrid-ове, менюта, всъщност всичко, което има смисъл да се използва многократно.
Как се вмъкват компоненти в презентера и след това се използват? Това ще научите в главата Компоненти. Дори ще разберете какво общо имат с Холивуд.
А къде мога да намеря компоненти? На страницата Componette ще намерите open-source компоненти, както и редица други добавки за 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
, фреймуъркът определя един от тях за
основен (каноничен) и останалите пренасочва към него с помощта на HTTP
код 301. Благодарение на това търсачките не индексират страниците ви
два пъти и не размиват техния page rank.
Този процес се нарича канонизация. Каноничният 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. На разположение са редица готови отговори:
- Nette\Application\Responses\CallbackResponse – изпраща callback
- Nette\Application\Responses\FileResponse – изпраща файл
- Nette\Application\Responses\ForwardResponse – forward()
- Nette\Application\Responses\JsonResponse – изпраща JSON
- Nette\Application\Responses\RedirectResponse – пренасочване
- Nette\Application\Responses\TextResponse – изпраща текст
- Nette\Application\Responses\VoidResponse – празен отговор
Отговорите се изпращат с метода sendResponse()
:
use Nette\Application\Responses;
// Обикновен текст
$this->sendResponse(new Responses\TextResponse('Hello Nette!'));
// Изпраща файл
$this->sendResponse(new Responses\FileResponse(__DIR__ . '/invoice.pdf', 'Invoice13.pdf'));
// Отговорът ще бъде callback
$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 заявка,
ограничаване до същия произход (same origin) и достъп само чрез пренасочване
(forward). Атрибутът може да се прилага както към класове на презентери,
така и към отделни методи action<Action>()
, render<View>()
,
handle<Signal>()
и createComponent<Name>()
.
Можете да посочите следните ограничения:
- на HTTP методи:
#[Requires(methods: ['GET', 'POST'])]
- изискване на AJAX заявка:
#[Requires(ajax: true)]
- достъп само от същия произход:
#[Requires(sameOrigin: true)]
- достъп само чрез forward:
#[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 request, който
браузърът автоматично изпраща преди реалната заявка, когато е
необходимо да се провери дали заявката е разрешена от гледна точка на
CORS (Cross-Origin Resource Sharing) политиката. Ако разрешите метода, но не
имплементирате правилен отговор, това може да доведе до
неконсистентности и потенциални проблеми със сигурността.