Презентери
Навчимося створювати презентери та шаблони в Nette. Після прочитання цієї статті ви дізнаєтеся:
- як працює презентер
- що таке постійні параметри
- як відрендерити шаблон
Ми вже знаємо, що презентер – це клас, який представляє конкретну сторінку веб-додатка, як-от головна сторінка; сторінка товару в інтернет-магазині; форма авторизації; мапа сайту тощо. Додаток може мати від одного до тисячі презентерів. В інших фреймворках вони також відомі як контролери.
Зазвичай термін презентер співвідноситься з нащадком класу 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)
виходить із презентера і надсилає дані у форматі JSONsendTemplate()
завершує роботу презентера і відразу ж виконує рендеринг шаблону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
, фреймворк призначає одну з них
основною (канонічною) і перенаправляє на неї решту за допомогою HTTP-коду
301. Завдяки цьому пошукові системи не індексують сторінки двічі і не
послаблюють їхній рейтинг.
Цей процес називається канонізацією. Канонічний 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. Є кілька готових відповідей:
- Nette\Application\Responses\CallbackResponse – надсилає зворотний виклик
- 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 = function (Nette\Http\IRequest $httpRequest, Nette\Http\IResponse $httpResponse) {
if ($httpResponse->getHeader('Content-Type') === 'text/html') {
echo '<h1>Привіт</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
, ви також
повинні правильно обробляти його у вашому презентері. Цей метод часто
використовується як так званий попередній запит, який браузери
автоматично надсилають перед самим запитом, коли необхідно визначити,
чи дозволений запит з точки зору політики CORS (Cross-Origin Resource Sharing). Якщо ви
дозволите цей метод, але не реалізуєте відповідну реакцію, це може
призвести до невідповідностей і потенційних проблем з безпекою.
Читати далі
- Введення методів та атрибутів
- Складання презентерів затрибутів
- Передача налаштуваньпрезентерам
- Як повернутися на попередню сторінку