Nette Documentation Preview

syntax
Презентеры
**********

<div class=perex>

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

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

</div>

[Мы уже знаем |how-it-works#Nette Application], что презентер - класс, представляющий определённую страницу веб-приложения, например главную страницу, товар в интернет-магазине, форму входа, ленту карты сайта и так далее. У приложения может быть от одного до тысяч презентеров. В других фреймворках их называют контроллерами.

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


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

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

Итак, сначала ему передаётся запрос. Это не прямой HTTP-запрос, а объект [api:Nette\Application\Request], в который HTTP-запрос был преобразован с помощью маршрутизатора. Обычно мы с этим объектом напрямую не работаем, потому что презентер ловко передаёт обработку запроса другим методам, которые мы сейчас и рассмотрим.

[* lifecycle.svg *] *** Жизненный цикл презентера .<>

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


`__construct()`
---------------

Строго говоря, конструктор не относится к жизненному циклу презентера, потому что вызывается в момент создания объекта. Но мы упоминаем его из-за его важности. Конструктор (вместе с [методом inject|best-practices:inject-method-attribute]) служит для передачи зависимостей.

Презентер не должен заниматься бизнес-логикой приложения, писать в базу данных или читать из неё, выполнять вычисления и подобное. За это отвечают классы слоя, который мы называем моделью. Например, класс `ArticleRepository` может отвечать за загрузку и сохранение статей. Чтобы презентер мог с ним работать, класс нужно [передать через внедрение зависимостей |dependency-injection:passing-dependencies]:


```php
class ArticlePresenter extends Nette\Application\UI\Presenter
{
	public function __construct(
		private ArticleRepository $articles,
	) {
	}
}
```


`startup()`
-----------

Сразу после получения запроса вызывается метод `startup()`. Вы можете использовать его для инициализации свойств, проверки прав пользователя и подобного. Требуется, чтобы этот метод всегда вызывал родительский: `parent::startup()`.


`action<Action>(args...)` .{toc: action<Action>()}
--------------------------------------------------

Похож на метод `render<View>()`. Если `render<View>()` предназначен для подготовки данных конкретного шаблона, который затем будет отрисован, то `action<Action>()` обрабатывает запрос, не обязательно отрисовывая после этого шаблон. Например, он может обработать данные, выполнить вход или выход пользователя и затем [перенаправить куда-то |#Перенаправление].

Важно, что `action<Action>()` вызывается *перед* `render<View>()`. Это позволяет нам при необходимости изменить ход запроса внутри метода действия, например поменять шаблон, который будет отрисован, или даже метод `render<View>()`, который будет вызван, с помощью `setView('otherView')`.

.{data-version:3.2.3}
Вы можете даже переключиться на совершенно другое действие методом `switch('otherAction')`. Он прерывает текущий метод и вместо этого запускает методы `action<Action>()` и `render<View>()` нового действия (и отключает автоматическую [канонизацию|#Канонизация]). Сам запрос продолжается; прерывается лишь выполняющийся в данный момент метод.

В метод передаются параметры из запроса. У этих параметров можно и рекомендуется указывать типы, например `actionShow(int $id, ?string $slug = null)`. Если параметра `id` нет или он не является целым числом, презентер возвращает [ошибку 404 |#Ошибка 404 и подобные] и завершается.


`handle<Signal>(args...)` .{toc: handle<Signal>()}
--------------------------------------------------

Этот метод обрабатывает так называемые сигналы, о которых мы узнаем в главе, посвящённой [компонентам |components#Сигнал]. Он предназначен прежде всего для компонентов и обработки AJAX-запросов.

В метод передаются параметры из запроса, как и в `action<Action>()`, включая проверку типов.


`beforeRender()`
----------------

Метод `beforeRender`, как следует из его имени, вызывается перед каждым методом `render<View>()`. Он служит для общей настройки шаблона, передачи переменных в макет и подобных задач.


`render<View>(args...)` .{toc: render<View>()}
----------------------------------------------

Здесь мы готовим шаблон к последующей отрисовке, передаём в него данные и так далее.

В метод передаются параметры из запроса, как и в `action<Action>()`, включая проверку типов.

```php
public function renderShow(int $id): void
{
	// получаем данные из модели и передаём их в шаблон
	$this->template->article = $this->articles->getById($id);
}
```


`afterRender()`
---------------

Метод `afterRender`, как опять же следует из имени, вызывается после каждого метода `render<View>()`. Используется он довольно редко.


`shutdown()`
------------

Вызывается в конце жизненного цикла презентера.


События
-------

Помимо методов `startup()`, `beforeRender()` и `shutdown()`, вызываемых в рамках жизненного цикла презентера, можно определить и другие функции, которые будут вызваны автоматически. Презентер определяет так называемые [события |nette:glossary#События], и вы добавляете их обработчики в массивы `$onStartup`, `$onRender` и `$onShutdown`.

```php
class ArticlePresenter extends Nette\Application\UI\Presenter
{
	public function __construct()
	{
		$this->onStartup[] = function () {
			// ...
		};
	}
}
```

Обработчики из массива `$onStartup` вызываются прямо перед методом `startup()`, обработчики `$onRender` - между `beforeRender()` и `render<View>()`, а обработчики `$onShutdown` - прямо перед `shutdown()`.


**Небольшой совет, прежде чем продолжим:** как видите, презентер может обрабатывать несколько действий или представлений, то есть у него может быть несколько методов `render<View>()`. Однако мы рекомендуем проектировать презентеры с одним действием или с как можно меньшим их числом.


Отправка ответа
===============

Ответом презентера обычно служит [отрисовка шаблона в HTML-страницу|templates], но это может быть и отправка файла, JSON или даже перенаправление на другую страницу.

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

- `redirect()`, `redirectPermanent()`, `redirectUrl()` и `forward()` выполняют [перенаправление |#Перенаправление]
- `error()` завершает презентер [из-за ошибки |#Ошибка 404 и подобные]
- `sendJson($data)` завершает презентер и [отправляет данные |#Отправка JSON] в формате JSON
- `sendTemplate()` завершает презентер и сразу [отрисовывает шаблон |templates]
- `sendResponse($response)` завершает презентер и отправляет [собственный ответ |#Ответы]
- `terminate()` завершает презентер без ответа

Каждый из этих методов немедленно завершает презентер, выбрасывая исключение молчаливого завершения `Nette\Application\AbortException`.

Если вы не вызовете ни один из этих методов, презентер автоматически перейдёт к отрисовке шаблона. Почему? Потому что в 99 % случаев мы хотим отрисовать шаблон, поэтому презентер принимает такое поведение как поведение по умолчанию, чтобы упростить нам работу.


Создание ссылок
===============

У презентера есть метод `link()`, служащий для создания URL-ссылок на другие презентеры. Первый параметр - целевой презентер и действие, за ними идут аргументы, которые можно передать массивом:

```php
$url = $this->link('Product:show', $id);

$url = $this->link('Product:show', [$id, 'lang' => 'en']);
```

В шаблоне ссылки на другие презентеры и действия создаются так:

```latte
<a n:href="Product:show $id">product detail</a>
```

Просто напишите привычную пару `Презентер:действие` вместо настоящего URL и добавьте нужные параметры. Хитрость в `n:href`, которая говорит Latte обработать этот атрибут и породить настоящий URL. В Nette вам вообще не нужно думать об URL, только о презентерах и действиях.

Подробнее в главе [Создание URL-ссылок|creating-links].


Перенаправление
===============

Для перехода к другому презентеру служат методы `redirect()` и `forward()`. Их синтаксис очень похож на метод [link() |#Создание ссылок].

Метод `forward()` переходит к новому презентеру сразу, без HTTP-перенаправления:

```php
$this->forward('Product:show');
```

Пример временного перенаправления с HTTP-кодом 302 (или 303, если текущий метод запроса - POST):

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

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

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

Перенаправить на другой URL вне приложения можно методом `redirectUrl()`. HTTP-код можно указать вторым параметром; по умолчанию это 302 (или 303, если текущий метод запроса - POST):

```php
$this->redirectUrl('https://nette.org');
```

Перенаправление немедленно завершает работу презентера, выбрасывая так называемое исключение молчаливого завершения `Nette\Application\AbortException`.

Перед перенаправлением можно отправить [flash-сообщения |#Flash-сообщения], то есть сообщения, которые отобразятся в шаблоне после перенаправления.


Flash-сообщения
===============

Это сообщения, обычно извещающие о результате какой-то операции. Важная особенность flash-сообщений в том, что они остаются доступны в шаблоне и после перенаправления. После показа они остаются активными ещё 30 секунд: например, если пользователь обновит страницу из-за ошибки передачи, сообщение не исчезнет сразу.

Достаточно вызвать метод [flashMessage() |api:Nette\Application\UI\Control::flashMessage()], а презентер позаботится о передаче сообщения в шаблон. Первый параметр - текст сообщения, необязательный второй - его тип (например, error, warning, info). Метод `flashMessage()` возвращает экземпляр flash-сообщения, что позволяет добавить дополнительные сведения.

```php
$this->flashMessage('The item has been deleted.');
$this->redirect(/* ... */); // и перенаправляем
```

В шаблоне эти сообщения доступны в переменной `$flashes` как объекты `stdClass`, содержащие свойства `message` (текст сообщения), `type` (тип сообщения) и, возможно, упомянутые добавленные пользователем сведения. Отрисовываем мы их так:

```latte
{foreach $flashes as $flash}
	<div class="flash {$flash->type}">{$flash->message}</div>
{/foreach}
```


Ошибка 404 и подобные
=====================

Если запрос выполнить нельзя, например потому что статьи, которую мы хотим показать, нет в базе данных, мы выбрасываем ошибку 404 методом `error(string $message = '', int $httpCode = 404)`.

```php
public function renderShow(int $id): void
{
	$article = $this->articles->getById($id);
	if (!$article) {
		$this->error();
	}
	// ...
}
```

HTTP-код ошибки можно передать вторым параметром; по умолчанию это 404. Метод работает так, что выбрасывает `Nette\Application\BadRequestException`, после чего `Application` передаёт управление error-презентеру. Это презентер, задача которого - показать страницу с сообщением о произошедшей ошибке. Error-презентер задаётся в [конфигурации приложения|configuration].


Отправка JSON
=============

Метод `sendJson($data)` кодирует заданные данные в JSON, отправляет их как HTTP-ответ и завершает презентер. Пример:

```php
public function actionData(): void
{
	$data = ['hello' => 'nette'];
	$this->sendJson($data);
}
```


Параметры запроса .{data-version:3.1.14}
========================================

Презентер, а также каждый компонент получают свои параметры из HTTP-запроса. Получить их значения можно методами `getParameter($name)` или `getParameters()`. Значениями служат строки или массивы строк, по сути сырые данные, полученные прямо из URL.

Ради большего удобства мы рекомендуем обращаться к параметрам через свойства. Достаточно пометить их атрибутом `#[Parameter]`:

```php
use Nette\Application\Attributes\Parameter;  // эта строка важна

class HomePresenter extends Nette\Application\UI\Presenter
{
	#[Parameter]
	public string $theme; // должно быть public
}
```

У свойства мы рекомендуем указывать тип данных (например, `string`), и Nette автоматически приведёт значение соответствующим образом. Значения параметров можно и [проверять |#Проверка параметров].

При создании ссылки значение параметра можно задать напрямую:

```latte
<a n:href="Home:default theme: dark">click</a>
```


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

Постоянные параметры служат для сохранения состояния между разными запросами. Их значение остаётся тем же и после щелчка по ссылке. В отличие от данных сессии, они передаются в URL. И происходит это полностью автоматически, поэтому явно указывать их в `link()` или `n:href` не нужно.

Пример применения? Представьте многоязычное приложение. Текущий язык - параметр, который всегда должен быть частью URL. Но включать его в каждую ссылку было бы невероятно утомительно. Поэтому вы делаете его постоянным параметром `lang`, и он будет переноситься автоматически. Красота!

Создать постоянный параметр в Nette исключительно просто. Достаточно создать публичное свойство и пометить его атрибутом (раньше использовалось `/** @persistent */`):

```php
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`), а также можно задать значение по умолчанию. Значения параметров можно [проверять |#Проверка параметров].

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

- в общем предке, от которого наследуются презентеры
- либо в трейте, который презентеры используют:

```php
trait LanguageAware
{
	#[Persistent]
	public string $lang;
}

class ProductPresenter extends Nette\Application\UI\Presenter
{
	use LanguageAware;
}
```

При создании ссылки значение постоянного параметра можно изменить:

```latte
<a n:href="Product:show $id, lang: cs">detail in Czech</a>
```

Как вариант, его можно *сбросить*, то есть убрать из URL. Тогда он примет значение по умолчанию:

```latte
<a n:href="Product:show $id, lang: null">click</a>
```


Общее пространство параметров
=============================

Параметры запроса, [постоянные параметры |#Постоянные параметры] и параметры методов `action`, `render` и `handle` (сигналов) делят единое пространство, где каждый определяется своим именем. Если одно и то же имя встречается больше чем в одном из них, они относятся к одному и тому же значению.

Этим часто пользуются. Например, постоянный параметр `lang` и аргумент `$lang` метода действия или сигнала - одно и то же: вы можете прочитать текущее значение постоянного параметра, просто указав его в сигнатуре метода:

```php
#[Persistent]
public string $lang;

public function handleSearch(string $query, string $lang): void
{
	// $lang содержит текущее значение постоянного параметра lang
}
```

Поскольку это пространство общее, держите имена параметров уникальными, если только вы намеренно не хотите, чтобы они делили значение. Это относится и к сигналам, которые дополнительно читают параметры из тела POST-запроса, см. [Сигналы в подробностях |components#Сигналы в подробностях].


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

В презентеры встроена система компонентов. Компоненты - самостоятельные переиспользуемые единицы, которые мы встраиваем в презентеры. Это могут быть [формы |forms:in-presenter], таблицы данных, меню - в общем, всё, что имеет смысл использовать повторно.

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

А где взять компоненты? На [Componette |https://componette.org/search/component] вы найдёте компоненты с открытым кодом и множество других дополнений для Nette, созданных добровольцами из сообщества фреймворка.


Погружаемся глубже
==================

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


Проверка параметров
-------------------

Значения [параметров запроса |#Параметры запроса] и [постоянных параметров |#Постоянные параметры], полученные из URL, записываются в свойства методом `loadState()`. Он также проверяет, соответствуют ли они типу данных, указанному у свойства; иначе он ответит ошибкой 404, и страница не отобразится.

Никогда не доверяйте параметрам из URL вслепую, потому что пользователь легко может их переписать. Вот, например, как мы проверили бы, входит ли язык `$this->lang` в число поддерживаемых. Подходящий способ - переопределить упомянутый метод `loadState()`:

```php
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();
		}
	}
}
```


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

Запрос, обрабатываемый презентером, - объект [api:Nette\Application\Request], возвращаемый методом презентера `getRequest()`.

Текущий запрос можно сохранить в сессию или, наоборот, восстановить из неё и дать презентеру выполнить его заново. Это полезно, например, когда пользователь заполняет форму, а его сессия входа истекает. Чтобы не потерять данные, перед перенаправлением на страницу входа мы сохраняем текущий запрос в сессию через `$reqId = $this->storeRequest()`. Это возвращает его идентификатор в виде короткой строки, которую мы затем передаём параметром презентеру входа.

После входа мы вызываем метод `$this->restoreRequest($reqId)`, который достаёт запрос из сессии. POST-запросы перебрасываются в него, а остальные (GET) перенаправляются на URL запроса. Метод проверяет, что запрос был создан тем же пользователем, который сейчас вошёл. Если войдёт другой пользователь или ключ недействителен, метод ничего не делает, и программа продолжает работу как обычно.

См. руководство [Как вернуться на предыдущую страницу |best-practices:restore-request].


Канонизация
-----------

У презентеров есть по-настоящему прекрасная возможность, способствующая лучшему SEO (поисковой оптимизации). Они автоматически предотвращают существование одинакового содержимого под разными URL. Если к определённой цели ведут несколько URL, например `/index` и `/index?page=1`, фреймворк объявляет один из них основным (каноническим) и перенаправляет остальные на него HTTP-кодом 301. Благодаря этому поисковые системы не индексируют ваши страницы дважды и не размывают их вес.

Этот процесс называется канонизацией. Канонический URL - тот, который порождает [маршрутизатор|routing], обычно первый подходящий маршрут в наборе.

Канонизация включена по умолчанию и может быть отключена через `$this->autoCanonicalize = false`.

Перенаправление не происходит при AJAX- или POST-запросах, потому что это могло бы привести к потере данных или не дало бы никакой пользы для SEO.

Вы можете вызвать канонизацию и вручную, методом `canonicalize()`. Как и методу `link()`, вы передаёте ему презентер, действие и параметры. Он порождает ссылку и сравнивает её с текущим URL-адресом. Если они различаются, он перенаправляет на порождённую ссылку.

```php
public function actionShow(int $id, ?string $slug = null): void
{
	$realSlug = $this->facade->getSlugForId($id);
	// перенаправляет, если $slug отличается от $realSlug
	$this->canonicalize('Product:show', [$id, $realSlug]);
}
```

Полный пример, объединяющий фильтры маршрутов с `canonicalize()` ради дружественных к SEO URL, см. в [Красивые URL со слагами |best-practices:pretty-urls].


Ответы
------

Ответ, возвращаемый презентером, - объект, реализующий интерфейс [api:Nette\Application\Response]. Доступно несколько готовых ответов:

- [api:Nette\Application\Responses\CallbackResponse] - отправляет callback
- [api:Nette\Application\Responses\FileResponse] - отправляет файл
- [api:Nette\Application\Responses\ForwardResponse] - forward()
- [api:Nette\Application\Responses\JsonResponse] - отправляет JSON
- [api:Nette\Application\Responses\RedirectResponse] - перенаправление
- [api:Nette\Application\Responses\TextResponse] - отправляет текст
- [api:Nette\Application\Responses\VoidResponse] - пустой ответ

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

```php
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));
```

Вы можете написать и собственный ответ. Достаточно реализовать интерфейс `Nette\Application\Response` с единственным методом `send()`, получающим HTTP-запрос и ответ. Это полезно, например, при потоковой передаче данных, которые вы не хотите держать в памяти:

```php
class CsvResponse implements Nette\Application\Response
{
	public function __construct(
		private string $fileName,
		private iterable $rows,
	) {
	}

	public function send(Nette\Http\IRequest $request, Nette\Http\IResponse $response): void
	{
		$response->setContentType('text/csv', 'utf-8');
		$response->sendAsFile($this->fileName);

		$handle = fopen('php://output', 'w');
		foreach ($this->rows as $row) {
			fputcsv($handle, $row);
		}

		fclose($handle);
	}
}
```

Затем вы отправляете его в презентере как обычно: `$this->sendResponse(new CsvResponse('export.csv', $rows));`


HTTP-кеширование
----------------

Метод `lastModified()` позволяет легко воспользоваться HTTP-кешированием. Вы передаёте ему дату и время последнего изменения содержимого (как временную метку, строку или объект `DateTimeInterface`), а при желании ещё валидатор ETag (короткую строку, определяющую текущую версию содержимого, например её хеш) и время истечения. Если у браузера уже есть подходящая версия, презентер отправляет ответ `304 Not Modified` и завершается, поэтому страница не отрисовывается и не передаётся зря:

```php
public function renderArticle(int $id): void
{
	$article = $this->articles->getById($id);
	$this->lastModified($article->updatedAt);
	// ...
}
```


Завершение шаблона .{data-version:3.3.0}
----------------------------------------

Когда презентер отрисовывает шаблон, метод `sendTemplate()` прямо перед отрисовкой вызывает `completeTemplate()`. Этот метод заполняет переменные, помеченные атрибутом `#[TemplateVariable]`, и находит файл шаблона (переменные по умолчанию уже задаёт `TemplateFactory` при создании шаблона). Вы можете переопределить этот защищённый метод, чтобы добавить переменные, общие для всех представлений, или задать другой файл:

```php
protected function completeTemplate(Nette\Application\UI\Template $template): void
{
	parent::completeTemplate($template);
	$template->siteName = 'My App';
}
```


Ограничение доступа через `#[Requires]` .{data-version:3.2.3}
-------------------------------------------------------------

Атрибут `#[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')]`

.[note]
Начиная с версии 3.3 совпадение источника проверяется по заголовку браузера `Sec-Fetch-Site` (раньше через cookie SameSite), что надёжнее и проверяет точное совпадение схемы, домена и порта.

Подробности в руководстве [Как использовать атрибут Requires |best-practices:attribute-requires].


Проверка HTTP-метода
--------------------

Презентеры в Nette автоматически проверяют HTTP-метод каждого входящего запроса, прежде всего из соображений безопасности. По умолчанию разрешены методы `GET`, `POST`, `HEAD`, `PUT`, `DELETE`, `PATCH`.

Если вы хотите дополнительно разрешить, например, метод `OPTIONS`, используйте атрибут `#[Requires]` (начиная с Nette Application v3.2.3):

```php
#[Requires(methods: ['GET', 'POST', 'HEAD', 'PUT', 'DELETE', 'PATCH', 'OPTIONS'])]
class MyPresenter extends Nette\Application\UI\Presenter
{
}
```

Начиная с версии 3.1.13 проверка выполняется в `checkHttpMethod()`, который проверяет, входит ли указанный в запросе метод в массив `$presenter->allowedMethods`. Начиная с версии 3.2.3 этот подход объявлен устаревшим в пользу `#[Requires]`. Переопределить метод можно так:

```php
class MyPresenter extends Nette\Application\UI\Presenter
{
	protected function checkHttpMethod(): void
	{
		$this->allowedMethods[] = 'OPTIONS';
		parent::checkHttpMethod();
	}
}
```

Важно подчеркнуть, что если вы разрешаете метод `OPTIONS`, вы должны затем соответствующим образом обработать его в своём презентере. Этот метод часто используется как так называемый предварительный (preflight) запрос, который браузер автоматически отправляет перед настоящим запросом, когда нужно определить, допустим ли запрос по политике CORS (Cross-Origin Resource Sharing). Если вы разрешите метод, но не реализуете правильный ответ, это может привести к несогласованности и возможным проблемам с безопасностью.


Пометка устаревших действий .{data-version:3.2.3}
-------------------------------------------------

Атрибут `#[Deprecated]` помечает действия, сигналы или целые презентеры как устаревшие и предназначенные к будущему удалению. При порождении ссылок на устаревшие части приложения Nette выдаёт предупреждение, чтобы обратить на это внимание разработчиков.

Атрибут можно применить как ко всему классу презентера, так и к отдельным методам `action<Action>()`, `render<View>()` и `handle<Signal>()`.


Дополнительные материалы
========================

- [Методы и атрибуты inject |best-practices:inject-method-attribute]
- [Составление презентеров из трейтов |best-practices:presenter-traits]
- [Передача настроек в презентеры |best-practices:passing-settings-to-presenters]
- [Как вернуться на предыдущую страницу |best-practices:restore-request]

Презентеры

Мы разберём, как в 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('otherView').

Вы можете даже переключиться на совершенно другое действие методом switch('otherAction'). Он прерывает текущий метод и вместо этого запускает методы action<Action>() и render<View>() нового действия (и отключает автоматическую канонизацию). Сам запрос продолжается; прерывается лишь выполняющийся в данный момент метод.

В метод передаются параметры из запроса. У этих параметров можно и рекомендуется указывать типы, например 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()

Вызывается в конце жизненного цикла презентера.

События

Помимо методов 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().

Небольшой совет, прежде чем продолжим: как видите, презентер может обрабатывать несколько действий или представлений, то есть у него может быть несколько методов render<View>(). Однако мы рекомендуем проектировать презентеры с одним действием или с как можно меньшим их числом.

Отправка ответа

Ответом презентера обычно служит отрисовка шаблона в HTML-страницу, но это может быть и отправка файла, JSON или даже перенаправление на другую страницу.

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

Каждый из этих методов немедленно завершает презентер, выбрасывая исключение молчаливого завершения Nette\Application\AbortException.

Если вы не вызовете ни один из этих методов, презентер автоматически перейдёт к отрисовке шаблона. Почему? Потому что в 99 % случаев мы хотим отрисовать шаблон, поэтому презентер принимает такое поведение как поведение по умолчанию, чтобы упростить нам работу.

Создание ссылок

У презентера есть метод link(), служащий для создания URL-ссылок на другие презентеры. Первый параметр – целевой презентер и действие, за ними идут аргументы, которые можно передать массивом:

$url = $this->link('Product:show', $id);

$url = $this->link('Product:show', [$id, 'lang' => 'en']);

В шаблоне ссылки на другие презентеры и действия создаются так:

<a n:href="Product:show $id">product detail</a>

Просто напишите привычную пару Презентер:действие вместо настоящего URL и добавьте нужные параметры. Хитрость в 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('The item has been deleted.');
$this->redirect(/* ... */); // и перенаправляем

В шаблоне эти сообщения доступны в переменной $flashes как объекты stdClass, содержащие свойства message (текст сообщения), type (тип сообщения) и, возможно, упомянутые добавленные пользователем сведения. Отрисовываем мы их так:

{foreach $flashes as $flash}
	<div class="flash {$flash->type}">{$flash->message}</div>
{/foreach}

Ошибка 404 и подобные

Если запрос выполнить нельзя, например потому что статьи, которую мы хотим показать, нет в базе данных, мы выбрасываем ошибку 404 методом error(string $message = '', 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-презентер задаётся в конфигурации приложения.

Отправка JSON

Метод sendJson($data) кодирует заданные данные в JSON, отправляет их как HTTP-ответ и завершает презентер. Пример:

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">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; // должно быть 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">detail in Czech</a>

Как вариант, его можно сбросить, то есть убрать из URL. Тогда он примет значение по умолчанию:

<a n:href="Product:show $id, lang: null">click</a>

Общее пространство параметров

Параметры запроса, постоянные параметры и параметры методов action, render и handle (сигналов) делят единое пространство, где каждый определяется своим именем. Если одно и то же имя встречается больше чем в одном из них, они относятся к одному и тому же значению.

Этим часто пользуются. Например, постоянный параметр lang и аргумент $lang метода действия или сигнала – одно и то же: вы можете прочитать текущее значение постоянного параметра, просто указав его в сигнатуре метода:

#[Persistent]
public string $lang;

public function handleSearch(string $query, string $lang): void
{
	// $lang содержит текущее значение постоянного параметра lang
}

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

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

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

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

А где взять компоненты? На 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), который достаёт запрос из сессии. POST-запросы перебрасываются в него, а остальные (GET) перенаправляются на URL запроса. Метод проверяет, что запрос был создан тем же пользователем, который сейчас вошёл. Если войдёт другой пользователь или ключ недействителен, метод ничего не делает, и программа продолжает работу как обычно.

См. руководство Как вернуться на предыдущую страницу.

Канонизация

У презентеров есть по-настоящему прекрасная возможность, способствующая лучшему SEO (поисковой оптимизации). Они автоматически предотвращают существование одинакового содержимого под разными URL. Если к определённой цели ведут несколько URL, например /index и /index?page=1, фреймворк объявляет один из них основным (каноническим) и перенаправляет остальные на него HTTP-кодом 301. Благодаря этому поисковые системы не индексируют ваши страницы дважды и не размывают их вес.

Этот процесс называется канонизацией. Канонический 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]);
}

Полный пример, объединяющий фильтры маршрутов с canonicalize() ради дружественных к SEO URL, см. в Красивые URL со слагами.

Ответы

Ответ, возвращаемый презентером, – объект, реализующий интерфейс 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));

Вы можете написать и собственный ответ. Достаточно реализовать интерфейс Nette\Application\Response с единственным методом send(), получающим HTTP-запрос и ответ. Это полезно, например, при потоковой передаче данных, которые вы не хотите держать в памяти:

class CsvResponse implements Nette\Application\Response
{
	public function __construct(
		private string $fileName,
		private iterable $rows,
	) {
	}

	public function send(Nette\Http\IRequest $request, Nette\Http\IResponse $response): void
	{
		$response->setContentType('text/csv', 'utf-8');
		$response->sendAsFile($this->fileName);

		$handle = fopen('php://output', 'w');
		foreach ($this->rows as $row) {
			fputcsv($handle, $row);
		}

		fclose($handle);
	}
}

Затем вы отправляете его в презентере как обычно: $this->sendResponse(new CsvResponse('export.csv', $rows));

HTTP-кеширование

Метод lastModified() позволяет легко воспользоваться HTTP-кешированием. Вы передаёте ему дату и время последнего изменения содержимого (как временную метку, строку или объект DateTimeInterface), а при желании ещё валидатор ETag (короткую строку, определяющую текущую версию содержимого, например её хеш) и время истечения. Если у браузера уже есть подходящая версия, презентер отправляет ответ 304 Not Modified и завершается, поэтому страница не отрисовывается и не передаётся зря:

public function renderArticle(int $id): void
{
	$article = $this->articles->getById($id);
	$this->lastModified($article->updatedAt);
	// ...
}

Завершение шаблона

Когда презентер отрисовывает шаблон, метод sendTemplate() прямо перед отрисовкой вызывает completeTemplate(). Этот метод заполняет переменные, помеченные атрибутом #[TemplateVariable], и находит файл шаблона (переменные по умолчанию уже задаёт TemplateFactory при создании шаблона). Вы можете переопределить этот защищённый метод, чтобы добавить переменные, общие для всех представлений, или задать другой файл:

protected function completeTemplate(Nette\Application\UI\Template $template): void
{
	parent::completeTemplate($template);
	$template->siteName = 'My App';
}

Ограничение доступа через #[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')]

Начиная с версии 3.3 совпадение источника проверяется по заголовку браузера Sec-Fetch-Site (раньше через cookie SameSite), что надёжнее и проверяет точное совпадение схемы, домена и порта.

Подробности в руководстве Как использовать атрибут Requires.

Проверка HTTP-метода

Презентеры в Nette автоматически проверяют HTTP-метод каждого входящего запроса, прежде всего из соображений безопасности. По умолчанию разрешены методы GET, POST, HEAD, PUT, DELETE, PATCH.

Если вы хотите дополнительно разрешить, например, метод OPTIONS, используйте атрибут #[Requires] (начиная с Nette Application v3.2.3):

#[Requires(methods: ['GET', 'POST', 'HEAD', 'PUT', 'DELETE', 'PATCH', 'OPTIONS'])]
class MyPresenter extends Nette\Application\UI\Presenter
{
}

Начиная с версии 3.1.13 проверка выполняется в checkHttpMethod(), который проверяет, входит ли указанный в запросе метод в массив $presenter->allowedMethods. Начиная с версии 3.2.3 этот подход объявлен устаревшим в пользу #[Requires]. Переопределить метод можно так:

class MyPresenter extends Nette\Application\UI\Presenter
{
	protected function checkHttpMethod(): void
	{
		$this->allowedMethods[] = 'OPTIONS';
		parent::checkHttpMethod();
	}
}

Важно подчеркнуть, что если вы разрешаете метод OPTIONS, вы должны затем соответствующим образом обработать его в своём презентере. Этот метод часто используется как так называемый предварительный (preflight) запрос, который браузер автоматически отправляет перед настоящим запросом, когда нужно определить, допустим ли запрос по политике CORS (Cross-Origin Resource Sharing). Если вы разрешите метод, но не реализуете правильный ответ, это может привести к несогласованности и возможным проблемам с безопасностью.

Пометка устаревших действий

Атрибут #[Deprecated] помечает действия, сигналы или целые презентеры как устаревшие и предназначенные к будущему удалению. При порождении ссылок на устаревшие части приложения Nette выдаёт предупреждение, чтобы обратить на это внимание разработчиков.

Атрибут можно применить как ко всему классу презентера, так и к отдельным методам action<Action>(), render<View>() и handle<Signal>().

Дополнительные материалы