Презентери
Ми ознайомимося з тим, як у Nette пишуться презентери та шаблони. Після прочитання ви будете знати:
- як працює презентер
- що таке персистентні параметри
- як відображаються шаблони
Ми вже знаємо, що презентер — це клас, який представляє певну конкретну сторінку веб-застосунку, наприклад, головну сторінку; продукт в інтернет-магазині; форму входу; стрічку sitemap тощо. Застосунок може мати від одного до тисяч презентерів. В інших фреймворках їх також називають контролерами.
Зазвичай під поняттям презентер мається на увазі нащадок класу 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('іншийView')
.
Методу передаються параметри із запиту. Можна і рекомендується
вказувати типи параметрів, наприклад, actionShow(int $id, ?string $slug = null)
–
якщо параметр id
буде відсутній або якщо він не буде цілим
числом, презентер поверне помилку 404 і завершить
роботу.
handle<Signal>(args...)
Метод обробляє так звані сигнали, з якими ми познайомимося в розділі, присвяченому компонентам. Він призначений переважно для компонентів та обробки AJAX-запитів.
Методу передаються параметри із запиту, як у випадку
action<Action>()
, включно з перевіркою типів.
beforeRender()
Метод beforeRender
, як випливає з назви, викликається перед кожним
методом render<View>()
. Використовується для спільної
конфігурації шаблону, передачі змінних для layout тощо.
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-презентеру. Це
презентер, завданням якого є відобразити сторінку, що інформує про
помилку. Налаштування error-презентера здійснюється в конфігурації 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 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 ви знайдете компоненти з відкритим кодом, а також багато інших доповнень для 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) та доступу лише через переадресацію. Атрибут можна
застосовувати як до класів презентерів, так і до окремих методів
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). Якщо ви дозволите метод, але не реалізуєте правильну відповідь,
це може призвести до невідповідностей та потенційних проблем
безпеки.