Презентеры
Научимся создавать презентеры и шаблоны в 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>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). Если разрешить этот метод, но не реализовать
соответствующий ответ, это может привести к несоответствиям и
потенциальным проблемам безопасности.