Презентеры

Научимся создавать презентеры и шаблоны в 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) выходит из презентера и отправляет данные в формате 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:

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

Для достижения постоянного перенаправления с HTTP-кодом 301 используйте:

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

Вы можете перенаправить на другой URL вне приложения с помощью метода redirectUrl():

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

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

Постоянные параметры передаются автоматически в ссылках. Это означает, что нам не нужно явно указывать их в каждом link() или n:href в шаблоне, но они все равно будут переданы.

Если ваше приложение имеет несколько языковых версий, то текущий язык является параметром, который всегда должен быть частью URL. И было бы невероятно утомительно упоминать об этом в каждой ссылке. С Nette в этом нет необходимости. Таким образом мы просто помечаем параметр lang как постоянный:

class ProductPresenter extends Nette\Application\UI\Presenter
{
	/** @persistent */
	public $lang;
}

Если текущее значение параметра lang равно 'en', то URL, созданный с помощью link() или n:href в шаблоне, будет содержать lang=en. Отлично!

Однако мы также можем добавить параметр lang и тем самым изменить его значение:

<a n:href="Product:show $id, lang: en">подробности на английском</a>

Или, наоборот, его можно удалить, установив в null:

<a n:href="Product:show $id, lang: null">нажмите здесь</a>

Постоянная переменная должна быть объявлена как public. Мы также можем указать значение по умолчанию. Если параметр имеет то же значение, что и значение по умолчанию, он не будет включен в URL.

Постоянство отражает иерархию классов презентеров, поэтому параметр, определенный в конкретном презентере или трейте, автоматически передается каждому презентеру, наследующему от него или использующему тот же самый трейт.

В PHP 8 вы также можете использовать атрибуты для маркировки постоянных параметров:

use Nette\Application\Attributes\Persistent;

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

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

У презентеров есть встроенная система компонентов. Компоненты — это отдельные многократно используемые единицы, которые мы помещаем в презентеры. Это могут быть формы, сетки данных, меню, в общем, всё, что имеет смысл использовать многократно.

Как размещаются и впоследствии используются компоненты в презентере? Это объясняется в главе компоненты. Вы даже узнаете, какое отношение они имеют к Голливуду.

Где можно приобрести некоторые компоненты? На странице Componette вы можете найти некоторые компоненты с открытым исходным кодом и другие дополнения для Nette, которые создаются и распространяются сообществом фреймворка Nette.

Углубляемся

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

Требования и параметры

Запрос, обрабатываемый презентером, является объектом Nette\Application\Request и возвращается методом презентера getRequest(). Он включает в себя массив параметров, каждый из которых принадлежит либо какому-то из компонентов, либо непосредственно презентеру (который на самом деле тоже является компонентом, хотя и специальным). Таким образом, Nette перераспределяет параметры и передается между отдельными компонентами (и презентером) путем вызова метода loadState(array $params), который более подробно описан в главе Компоненты. Параметры можно получить с помощью метода getParameters(): array, индивидуально используя getParameter($name). Значения параметров — это строки или массивы строк, в основном это необработанные данные, полученные непосредственно из URL.

Сохранение и восстановление запроса

Вы можете сохранить текущий запрос в сессии или восстановить его из сессии и позволить презентеру выполнить его снова. Это полезно, например, когда пользователь заполняет форму, а срок действия его логина истекает. Чтобы не потерять данные, перед перенаправлением на страницу регистрации мы сохраняем текущий запрос в сессию с помощью функции $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. Есть несколько готовых ответов:

Ответы отправляются методом sendResponse():

use Nette\Application\Responses;

// Plain text
$this->sendResponse(new Responses\TextResponse('Hello Nette!'));

// Sends a file
$this->sendResponse(new Responses\FileResponse(__DIR__ . '/invoice.pdf', 'Invoice13.pdf'));

// Sends a 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));