Презентеры

Мы познакомимся с тем, как в Nette пишутся презентеры и шаблоны. После прочтения вы будете знать:

  • как работает презентер
  • что такое персистентные параметры
  • как отрисовываются шаблоны

Мы уже знаем, что презентер — это класс, представляющий какую-либо конкретную страницу веб-приложения, например, главную страницу; товар в интернет-магазине; форму входа; фид карты сайта и т. д. Приложение может иметь от одного до тысяч презентеров. В других фреймворках их также называют контроллерами.

Обычно под понятием презентер подразумевается потомок класса Nette\Application\UI\Presenter, который подходит для генерации веб-интерфейсов и которому мы будем уделять внимание в оставшейся части этой главы. В общем смысле презентер — это любой объект, реализующий интерфейс Nette\Application\IPresenter.

Жизненный цикл презентера

Задача презентера — обработать запрос и вернуть ответ (это может быть HTML-страница, изображение, перенаправление и т. д.).

То есть вначале ему передается запрос. Это не непосредственно HTTP-запрос, а объект Nette\Application\Request, в который был преобразован HTTP-запрос с помощью маршрутизатора. С этим объектом мы обычно не сталкиваемся, так как презентер умно делегирует обработку запроса другим методам, которые мы сейчас покажем.

Жизненный цикл презентера

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

__construct()

Конструктор не совсем относится к жизненному циклу презентера, так как вызывается в момент создания объекта. Но мы упоминаем его из-за важности. Конструктор (вместе с методом inject) служит для передачи зависимостей.

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

Методу передаются параметры из запроса. Возможно и рекомендуется указывать типы параметров, например, 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 или, например, перенаправление на другую страницу.

В любой момент жизненного цикла мы можем одним из следующих методов отправить ответ и одновременно завершить работу презентера:

Если вы не вызовете ни один из этих методов, презентер автоматически приступит к отрисовке шаблона. Почему? Потому что в 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>

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

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

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

А где я могу получить компоненты? На странице 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. Доступно несколько готовых ответов:

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

Дальнейшее чтение

версия: 4.0