Nette Documentation Preview

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

<div class=perex>

Компоненты - самостоятельные переиспользуемые объекты, которые мы встраиваем в страницы. Это могут быть формы, таблицы данных, опросы - в общем, всё, что имеет смысл использовать повторно. Мы покажем:

- как использовать компоненты?
- как их писать?
- что такое сигналы?

</div>

В Nette встроена система компонентов. Нечто похожее может быть знакомо ветеранам Delphi или ASP.NET Web Forms; React или Vue.js построены на чём-то отдалённо похожем. Однако в мире PHP-фреймворков это уникальная возможность.

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

Вы можете встроить в презентер сколько угодно компонентов. А в некоторые компоненты можно встраивать другие компоненты. Так возникает дерево компонентов, корнем которого служит презентер.


Фабричные методы
================

Как компоненты вставляются в презентер и затем используются? Обычно через фабричные методы.

Фабрика компонента - изящный способ создавать компоненты только тогда, когда они действительно нужны (лениво, по требованию). Вся магия заключается в реализации метода с именем `createComponent<Name>()`, где `<Name>` - имя создаваемого компонента; этот метод создаёт и возвращает компонент.

```php .{file:DefaultPresenter.php}
class DefaultPresenter extends Nette\Application\UI\Presenter
{
	protected function createComponentPoll(): PollControl
	{
		$poll = new PollControl;
		$poll->items = $this->items;
		return $poll;
	}
}
```

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

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

Мы никогда не вызываем фабрики напрямую: они вызываются автоматически при первом использовании компонента. Благодаря этому компонент создаётся в нужный момент и только если он действительно нужен. Если мы компонент не используем (например, при AJAX-запросе, когда передаётся лишь часть страницы, или при кешировании шаблона), он вообще не создастся, что сэкономит производительность сервера.

```php .{file:DefaultPresenter.php}
// обращаемся к компоненту, и если это в первый раз,
// вызывается createComponentPoll(), который его создаёт
$poll = $this->getComponent('poll');
// альтернативная запись: $poll = $this['poll'];
```

В шаблоне компонент можно отрисовать тегом [{control} |#Отрисовка]. Поэтому передавать компоненты в шаблон вручную не нужно.

```latte
<h2>Please Vote</h2>

{control poll}
```

.[tip]
Для динамического создания переменного числа компонентов используйте [Multiplier |multiplier].

Фабричные методы `createComponent<Name>()` работают не только в презентерах. Тем же способом вы можете вложить компонент в другой компонент и собрать их в дерево - это удобно, например, для отдельно отрисовываемой формы внутри компонента.


Голливудский стиль
==================

Компоненты обычно используют бодрый приём, который мы любим называть голливудским стилем. Вы наверняка знаете штамп, который часто слышат участники кинопроб: "Не звоните нам, мы позвоним вам". Именно об этом и речь.

В Nette вместо того, чтобы постоянно задавать вопросы ("была ли форма отправлена?", "была ли она корректна?", "нажал ли пользователь эту кнопку?"), вы говорите фреймворку: "когда произойдёт вот это, вызови этот метод", и оставляете дальнейшую работу ему. Если вы программируете на JavaScript, этот стиль вам близко знаком. Вы пишете функции, которые вызываются, когда происходит определённое событие. И язык передаёт им нужные параметры.

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


Написание компонента
====================

Под словом "компонент" мы обычно понимаем потомка класса [api:Nette\Application\UI\Control]. (Точнее было бы говорить "controls", но в некоторых языках это слово имеет другое значение, и "компоненты" прижилось лучше.) Сам презентер [api:Nette\Application\UI\Presenter] тоже является потомком класса `Control`.

```php .{file:PollControl.php}
use Nette\Application\UI\Control;

class PollControl extends Control
{
}
```


Отрисовка
=========

Мы уже знаем, что для отрисовки компонента служит тег `{control componentName}`. На деле он вызывает метод `render()` компонента, в котором мы заботимся об отрисовке. У нас есть, как и в презентере, [шаблон Latte|templates] в переменной `$this->template`, куда мы передаём параметры. В отличие от презентера, мы обязаны указать файл шаблона и заставить его отрисоваться:

```php .{file:PollControl.php}
public function render(): void
{
	// вставляем в шаблон какие-то параметры
	$this->template->param = $value;
	// и отрисовываем его
	$this->template->render(__DIR__ . '/poll.latte');
}
```

Тег `{control}` позволяет передать параметры в метод `render()`:

```latte
{control poll $id, $message}
```

```php .{file:PollControl.php}
public function render(int $id, string $message): void
{
	// ...
}
```

Иногда компонент может состоять из нескольких частей, которые мы хотим отрисовывать по отдельности. Для каждой из них мы создаём собственный метод отрисовки, здесь в примере `renderPaginator()`:

```php .{file:PollControl.php}
public function renderPaginator(): void
{
	// ...
}
```

А в шаблоне вызываем его так:

```latte
{control poll:paginator}
```

Для лучшего понимания полезно знать, как этот тег превращается в PHP-код.

```latte
{control poll}
{control poll:paginator 123, 'hello'}
```

превращается в:

```php
$control->getComponent('poll')->render();
$control->getComponent('poll')->renderPaginator(123, 'hello');
```

Метод `getComponent()` возвращает компонент `poll`, а у этого компонента вызывается метод `render()` или `renderPaginator()`, если в теге после двоеточия указан другой метод отрисовки.

.[caution]
Осторожно: если в параметрах вне квадратных скобок появляется **`=>`**, все параметры будут упакованы в массив и переданы как первый аргумент:

```latte
{control poll, id: 123, message: 'hello'}
```

превращается в:

```php
$control->getComponent('poll')->render(['id' => 123, 'message' => 'hello']);
```

Отрисовка подкомпонента:

```latte
{control cartControl-someForm}
```

превращается в:

```php
$control->getComponent("cartControl-someForm")->render();
```

Компоненты, как и презентеры, автоматически передают в шаблоны несколько полезных переменных:

- `$basePath` - абсолютный путь URL к корневому каталогу (например, `/eshop`)
- `$baseUrl` - абсолютный URL корневого каталога (например, `http://localhost/eshop`)
- `$user` - объект, [представляющий пользователя |security:authentication]
- `$presenter` - текущий презентер
- `$control` - текущий компонент
- `$flashes` - массив [сообщений |#Flash-сообщения], отправленных функцией `flashMessage()`


Сигнал
======

Мы уже знаем, что навигация в приложении Nette состоит из ссылок или перенаправлений на пары `Презентер:действие`. Но что, если мы хотим просто выполнить действие на **текущей странице**? Например, изменить сортировку столбцов таблицы; удалить элемент; переключить светлый или тёмный режим; отправить форму; проголосовать в опросе и так далее.

Такой вид запроса называется сигналом. И так же как действия вызывают методы `action<Action>()` или `render<Action>()`, сигналы вызывают методы `handle<Signal>()`. Понятие действия (или представления) относится исключительно к презентерам, а сигналы касаются всех компонентов. А значит, и презентеров, потому что `UI\Presenter` - потомок `UI\Control`.

```php
public function handleClick(int $x, int $y): void
{
	// ... обработка сигнала ...
}
```

Ссылка, вызывающая сигнал, создаётся обычным способом, то есть в шаблоне атрибутом `n:href` или тегом `{link}`, а в коде методом `link()`. Подробнее в главе [Создание URL-ссылок |creating-links#Ссылки на сигнал].

```latte
<a n:href="click! $x, $y">click here</a>
```

Сигнал всегда вызывается на текущем презентере и действии; вызвать его на другом презентере или действии нельзя.

Таким образом, сигнал вызывает перезагрузку страницы точно так же, как исходный запрос, но дополнительно вызывает метод обработки сигнала с нужными параметрами. Если метода не существует, выбрасывается исключение [api:Nette\Application\UI\BadSignalException], которое показывается пользователю как страница ошибки 403 Forbidden.


Сниппеты и AJAX
===============

Сигналы могут немного напомнить вам AJAX: обработчики, вызываемые на текущей странице. И вы правы, сигналы действительно часто вызываются через AJAX, и затем в браузер передаются только изменившиеся части страницы. Они называются сниппетами. Подробнее на [странице, посвящённой AJAX |ajax].


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

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

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

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

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

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


Перенаправление после обработки сигнала
=======================================

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

```php
$this->redirect('this'); // перенаправляет на текущие презентер и действие
```

Поскольку компонент - переиспользуемый элемент, который обычно не должен быть напрямую связан с конкретными презентерами, методы `redirect()` и `link()` автоматически истолковывают параметр как сигнал компонента:

```php
$this->redirect('click'); // перенаправляет на сигнал 'click' того же компонента
```

Если вам нужно перенаправить на другой презентер или действие, это можно сделать через презентер:

```php
$this->getPresenter()->redirect('Product:show'); // перенаправляет на другой презентер или действие
```


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

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

Например, у вас есть компонент постраничного вывода содержимого. Таких компонентов на странице может быть несколько. И мы хотим, чтобы все компоненты после щелчка по ссылке остались на своей текущей странице. Поэтому мы делаем номер страницы (`page`) постоянным параметром.

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

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

class PaginatingControl extends Control
{
	#[Persistent]
	public int $page = 1; // должно быть public
}
```

Мы рекомендуем указывать тип данных свойства (например, `int`), а также можно задать значение по умолчанию. Значения параметров можно [проверять |#Проверка постоянных параметров].

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

```latte
<a n:href="this page: $page + 1">next</a>
```

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

```latte
<a n:href="this page: null">reset</a>
```


Постоянные компоненты
=====================

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

```php
use Nette\Application\Attributes\Persistent;

#[Persistent('calendar', 'poll')]
class DefaultPresenter extends Nette\Application\UI\Presenter
{
}
```

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

Более старая аннотация `@persistent` ещё работает, но объявлена устаревшей и выдаёт предупреждение:

```php
/**
 * @persistent(calendar, poll)
 */
class DefaultPresenter extends Nette\Application\UI\Presenter
{
}
```


Компоненты с зависимостями
==========================

Как создавать компоненты с зависимостями, не "захламляя" презентеры, которые будут их использовать? Благодаря продуманным возможностям DI-контейнера Nette, как и в случае с обычными сервисами, большую часть работы можно оставить фреймворку.

Возьмём для примера компонент, зависящий от сервиса `PollFacade`:

```php
class PollControl extends Control
{
	public function __construct(
		private int $id, // ID опроса, для которого мы создаём компонент
		private PollFacade $facade,
	) {
	}

	public function handleVote(int $voteId): void
	{
		$this->facade->vote($this->id, $voteId);
		// ...
	}
}
```

Если бы мы писали обычный сервис, обсуждать было бы нечего. DI-контейнер незаметно позаботился бы о передаче всех зависимостей. Однако с компонентами мы обычно обходимся созданием нового экземпляра прямо в презентере, в [фабричных методах |#Фабричные методы] `createComponent…()`. Но передавать в презентер все зависимости всех компонентов только для того, чтобы передать их дальше компонентам, утомительно. И сколько кода приходится писать...

Логично спросить: почему бы просто не зарегистрировать компонент как обычный сервис, передать его в презентер и затем возвращать в методе `createComponent…()`? Однако такой подход неуместен, потому что мы хотим иметь возможность при необходимости создавать компонент несколько раз.

Правильное решение - написать для компонента фабрику, то есть класс, который создаёт компонент за нас:

```php
class PollControlFactory
{
	public function __construct(
		private PollFacade $facade,
	) {
	}

	public function create(int $id): PollControl
	{
		return new PollControl($id, $this->facade);
	}
}
```

Эту фабрику мы регистрируем в нашем контейнере в конфигурации:

```neon
services:
	- PollControlFactory
```

и наконец используем её в своём презентере:

```php
class PollPresenter extends Nette\Application\UI\Presenter
{
	public function __construct(
		private PollControlFactory $pollControlFactory,
	) {
	}

	protected function createComponentPollControl(): PollControl
	{
		$pollId = 1; // мы можем передать свой параметр
		return $this->pollControlFactory->create($pollId);
	}
}
```

Прекрасно то, что Nette DI умеет [порождать |dependency-injection:factory] такие простые фабрики, поэтому вместо всего её кода вам достаточно написать её интерфейс:

```php
interface PollControlFactory
{
	public function create(int $id): PollControl;
}
```

И это всё. Nette внутренне реализует этот интерфейс и внедрит его в презентер, где мы сможем им пользоваться. Он волшебным образом добавит в наш компонент параметр `$id` и экземпляр класса `PollFacade`.


Компоненты в подробностях
=========================

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

1) Он отрисовывается в шаблоне
2) Он знает, [какую свою часть |ajax#Сниппеты] отрисовать при AJAX-запросе (сниппеты)
3) Он умеет хранить своё состояние в URL (постоянные параметры)
4) Он умеет реагировать на действия пользователя (сигналы)
5) Он образует иерархическую структуру (корнем которой служит презентер)

За каждую из этих функций отвечает один из классов в цепочке наследования. За отрисовку (1 + 2) отвечает [api:Nette\Application\UI\Control], за встраивание в [жизненный цикл |presenters#Жизненный цикл презентера] (3, 4) - класс [api:Nette\Application\UI\Component], а за создание иерархической структуры (5) - классы [Container и Component |component-model:].

```
Nette\ComponentModel\Component  { IComponent }
|
+- Nette\ComponentModel\Container  { IContainer }
	|
	+- Nette\Application\UI\Component  { SignalReceiver, StatePersistent }
		|
		+- Nette\Application\UI\Control  { Renderable }
			|
			+- Nette\Application\UI\Presenter  { IPresenter }
```


Жизненный цикл компонента
-------------------------

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


Проверка постоянных параметров
------------------------------

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

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

```php
class PaginatingControl extends Control
{
	#[Persistent]
	public int $page = 1;

	public function loadState(array $params): void
	{
		parent::loadState($params); // здесь задаётся $this->page
		// далее идёт собственная проверка значения:
		if ($this->page < 1) {
			$this->error();
		}
	}
}
```

Обратным процессом, то есть сбором значений из постоянных свойств, занимается метод `saveState()`.


Подключение к презентеру
------------------------

В момент, когда компонент становится частью иерархии презентера, вызываются его callback-функции, хранящиеся в массиве `$onAnchor`. С этого момента у компонента есть презентер, он может безопасно создавать ссылки, читать постоянные параметры и так далее.

```php
$control->onAnchor[] = function ($control): void {
	// у компонента теперь есть презентер
};
```


Сигналы в подробностях
----------------------

Сигнал вызывает перезагрузку страницы ровно так же, как исходный запрос (кроме случая вызова через AJAX), и вызывает метод `signalReceived($signal)`, реализация которого по умолчанию в классе `Nette\Application\UI\Component` пытается вызвать метод, составленный из слов `handle<Signal>`. Дальнейшая обработка зависит от объекта. Объекты, наследующие от `Component` (то есть `Control` и `Presenter`), реагируют попыткой вызвать метод `handle<Signal>` с нужными параметрами.

Иначе говоря: берётся определение функции `handle<Signal>` вместе со всеми параметрами, пришедшими с запросом, параметры из URL сопоставляются аргументам по имени, и делается попытка вызвать метод. Например, значение параметра `id` из URL передаётся как аргумент `$id`, `something` из URL - как `$something` и так далее. А если метода не существует, метод `signalReceived` выбрасывает [исключение |api:Nette\Application\UI\BadSignalException].

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

Сигнал может принять любой компонент, презентер или объект, который реализует интерфейс `SignalReceiver` и подключён к дереву компонентов.

Главными получателями сигналов будут `Presenters` и визуальные компоненты, наследующие от `Control`. Сигнал предназначен служить объекту знаком, что нужно что-то сделать: опрос должен засчитать голос пользователя, блок новостей должен раскрыться и показать вдвое больше новостей, форма была отправлена и должна обработать данные и так далее.

URL для сигнала создаётся методом [Component::link() |api:Nette\Application\UI\Component::link()]. Как параметр `$destination` мы передаём строку `{signal}!`, а как `$args` - массив аргументов, которые хотим передать сигналу. Сигнал всегда вызывается на текущем презентере и действии с текущими параметрами; параметры сигнала лишь добавляются. Кроме того, добавляется **параметр `?do`, задающий сигнал**.

Его формат - либо `{signal}`, либо `{signalReceiver}-{signal}`. `{signalReceiver}` - имя компонента в презентере. Поэтому в имени компонента нельзя использовать дефис: он служит для разделения имени компонента и сигнала, хотя таким способом можно вкладывать несколько компонентов друг в друга.

Метод [isSignalReceiver()|api:Nette\Application\UI\Presenter::isSignalReceiver()] проверяет, является ли компонент (первый аргумент) получателем сигнала (второй аргумент). Второй аргумент можно опустить, тогда проверяется, является ли компонент получателем какого-либо сигнала. Если второй параметр установлен в `true`, проверяется, является ли получателем указанный компонент или любой из его потомков.

На любом этапе, предшествующем `handle<Signal>`, мы можем выполнить сигнал вручную, вызвав метод [processSignal()|api:Nette\Application\UI\Presenter::processSignal()], который заботится об обработке сигнала: он берёт компонент, определённый как получатель сигнала (если получатель не указан, это сам презентер), и отправляет ему сигнал.

Пример:

```php
if ($this->isSignalReceiver($this, 'paging') || $this->isSignalReceiver($this, 'sorting')) {
	$this->processSignal();
}
```

Это выполняет сигнал досрочно, и повторно он вызван не будет.

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

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

  • как использовать компоненты?
  • как их писать?
  • что такое сигналы?

В Nette встроена система компонентов. Нечто похожее может быть знакомо ветеранам Delphi или ASP.NET Web Forms; React или Vue.js построены на чём-то отдалённо похожем. Однако в мире PHP-фреймворков это уникальная возможность.

При этом компоненты принципиально меняют подход к разработке приложений. Вы можете собирать страницы из заранее подготовленных единиц. Нужна таблица данных в вашей администраторской части? Найдите её на Componette, в хранилище дополнений с открытым кодом (не только компонентов) для Nette, и просто вставьте в презентер.

Вы можете встроить в презентер сколько угодно компонентов. А в некоторые компоненты можно встраивать другие компоненты. Так возникает дерево компонентов, корнем которого служит презентер.

Фабричные методы

Как компоненты вставляются в презентер и затем используются? Обычно через фабричные методы.

Фабрика компонента – изящный способ создавать компоненты только тогда, когда они действительно нужны (лениво, по требованию). Вся магия заключается в реализации метода с именем createComponent<Name>(), где <Name> – имя создаваемого компонента; этот метод создаёт и возвращает компонент.

class DefaultPresenter extends Nette\Application\UI\Presenter
{
	protected function createComponentPoll(): PollControl
	{
		$poll = new PollControl;
		$poll->items = $this->items;
		return $poll;
	}
}

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

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

Мы никогда не вызываем фабрики напрямую: они вызываются автоматически при первом использовании компонента. Благодаря этому компонент создаётся в нужный момент и только если он действительно нужен. Если мы компонент не используем (например, при AJAX-запросе, когда передаётся лишь часть страницы, или при кешировании шаблона), он вообще не создастся, что сэкономит производительность сервера.

// обращаемся к компоненту, и если это в первый раз,
// вызывается createComponentPoll(), который его создаёт
$poll = $this->getComponent('poll');
// альтернативная запись: $poll = $this['poll'];

В шаблоне компонент можно отрисовать тегом {control}. Поэтому передавать компоненты в шаблон вручную не нужно.

<h2>Please Vote</h2>

{control poll}

Для динамического создания переменного числа компонентов используйте Multiplier.

Фабричные методы createComponent<Name>() работают не только в презентерах. Тем же способом вы можете вложить компонент в другой компонент и собрать их в дерево – это удобно, например, для отдельно отрисовываемой формы внутри компонента.

Голливудский стиль

Компоненты обычно используют бодрый приём, который мы любим называть голливудским стилем. Вы наверняка знаете штамп, который часто слышат участники кинопроб: „Не звоните нам, мы позвоним вам“. Именно об этом и речь.

В Nette вместо того, чтобы постоянно задавать вопросы („была ли форма отправлена?“, „была ли она корректна?“, „нажал ли пользователь эту кнопку?“), вы говорите фреймворку: „когда произойдёт вот это, вызови этот метод“, и оставляете дальнейшую работу ему. Если вы программируете на JavaScript, этот стиль вам близко знаком. Вы пишете функции, которые вызываются, когда происходит определённое событие. И язык передаёт им нужные параметры.

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

Написание компонента

Под словом „компонент“ мы обычно понимаем потомка класса Nette\Application\UI\Control. (Точнее было бы говорить „controls“, но в некоторых языках это слово имеет другое значение, и „компоненты“ прижилось лучше.) Сам презентер Nette\Application\UI\Presenter тоже является потомком класса Control.

use Nette\Application\UI\Control;

class PollControl extends Control
{
}

Отрисовка

Мы уже знаем, что для отрисовки компонента служит тег {control componentName}. На деле он вызывает метод render() компонента, в котором мы заботимся об отрисовке. У нас есть, как и в презентере, шаблон Latte в переменной $this->template, куда мы передаём параметры. В отличие от презентера, мы обязаны указать файл шаблона и заставить его отрисоваться:

public function render(): void
{
	// вставляем в шаблон какие-то параметры
	$this->template->param = $value;
	// и отрисовываем его
	$this->template->render(__DIR__ . '/poll.latte');
}

Тег {control} позволяет передать параметры в метод render():

{control poll $id, $message}
public function render(int $id, string $message): void
{
	// ...
}

Иногда компонент может состоять из нескольких частей, которые мы хотим отрисовывать по отдельности. Для каждой из них мы создаём собственный метод отрисовки, здесь в примере renderPaginator():

public function renderPaginator(): void
{
	// ...
}

А в шаблоне вызываем его так:

{control poll:paginator}

Для лучшего понимания полезно знать, как этот тег превращается в PHP-код.

{control poll}
{control poll:paginator 123, 'hello'}

превращается в:

$control->getComponent('poll')->render();
$control->getComponent('poll')->renderPaginator(123, 'hello');

Метод getComponent() возвращает компонент poll, а у этого компонента вызывается метод render() или renderPaginator(), если в теге после двоеточия указан другой метод отрисовки.

Осторожно: если в параметрах вне квадратных скобок появляется =>, все параметры будут упакованы в массив и переданы как первый аргумент:

{control poll, id: 123, message: 'hello'}

превращается в:

$control->getComponent('poll')->render(['id' => 123, 'message' => 'hello']);

Отрисовка подкомпонента:

{control cartControl-someForm}

превращается в:

$control->getComponent("cartControl-someForm")->render();

Компоненты, как и презентеры, автоматически передают в шаблоны несколько полезных переменных:

  • $basePath – абсолютный путь URL к корневому каталогу (например, /eshop)
  • $baseUrl – абсолютный URL корневого каталога (например, http://localhost/eshop)
  • $user – объект, представляющий пользователя
  • $presenter – текущий презентер
  • $control – текущий компонент
  • $flashes – массив сообщений, отправленных функцией flashMessage()

Сигнал

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

Такой вид запроса называется сигналом. И так же как действия вызывают методы action<Action>() или render<Action>(), сигналы вызывают методы handle<Signal>(). Понятие действия (или представления) относится исключительно к презентерам, а сигналы касаются всех компонентов. А значит, и презентеров, потому что UI\Presenter – потомок UI\Control.

public function handleClick(int $x, int $y): void
{
	// ... обработка сигнала ...
}

Ссылка, вызывающая сигнал, создаётся обычным способом, то есть в шаблоне атрибутом n:href или тегом {link}, а в коде методом link(). Подробнее в главе Создание URL-ссылок.

<a n:href="click! $x, $y">click here</a>

Сигнал всегда вызывается на текущем презентере и действии; вызвать его на другом презентере или действии нельзя.

Таким образом, сигнал вызывает перезагрузку страницы точно так же, как исходный запрос, но дополнительно вызывает метод обработки сигнала с нужными параметрами. Если метода не существует, выбрасывается исключение Nette\Application\UI\BadSignalException, которое показывается пользователю как страница ошибки 403 Forbidden.

Сниппеты и AJAX

Сигналы могут немного напомнить вам AJAX: обработчики, вызываемые на текущей странице. И вы правы, сигналы действительно часто вызываются через AJAX, и затем в браузер передаются только изменившиеся части страницы. Они называются сниппетами. Подробнее на странице, посвящённой AJAX.

Flash-сообщения

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

Отправкой занимается метод flashMessage. Первый параметр – текст сообщения (string, Stringable) или объект stdClass, представляющий сообщение. Необязательный второй параметр – его тип (error, warning, info и так далее). Метод flashMessage() возвращает экземпляр flash-сообщения как объект stdClass, в который можно добавить дополнительные сведения.

$this->flashMessage('Item was deleted.');
$this->redirect(/* ... */); // и перенаправляем

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

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

Перенаправление после обработки сигнала

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

$this->redirect('this'); // перенаправляет на текущие презентер и действие

Поскольку компонент – переиспользуемый элемент, который обычно не должен быть напрямую связан с конкретными презентерами, методы redirect() и link() автоматически истолковывают параметр как сигнал компонента:

$this->redirect('click'); // перенаправляет на сигнал 'click' того же компонента

Если вам нужно перенаправить на другой презентер или действие, это можно сделать через презентер:

$this->getPresenter()->redirect('Product:show'); // перенаправляет на другой презентер или действие

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

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

Например, у вас есть компонент постраничного вывода содержимого. Таких компонентов на странице может быть несколько. И мы хотим, чтобы все компоненты после щелчка по ссылке остались на своей текущей странице. Поэтому мы делаем номер страницы (page) постоянным параметром.

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

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

class PaginatingControl extends Control
{
	#[Persistent]
	public int $page = 1; // должно быть public
}

Мы рекомендуем указывать тип данных свойства (например, int), а также можно задать значение по умолчанию. Значения параметров можно проверять.

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

<a n:href="this page: $page + 1">next</a>

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

<a n:href="this page: null">reset</a>

Постоянные компоненты

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

use Nette\Application\Attributes\Persistent;

#[Persistent('calendar', 'poll')]
class DefaultPresenter extends Nette\Application\UI\Presenter
{
}

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

Более старая аннотация @persistent ещё работает, но объявлена устаревшей и выдаёт предупреждение:

/**
 * @persistent(calendar, poll)
 */
class DefaultPresenter extends Nette\Application\UI\Presenter
{
}

Компоненты с зависимостями

Как создавать компоненты с зависимостями, не „захламляя“ презентеры, которые будут их использовать? Благодаря продуманным возможностям DI-контейнера Nette, как и в случае с обычными сервисами, большую часть работы можно оставить фреймворку.

Возьмём для примера компонент, зависящий от сервиса PollFacade:

class PollControl extends Control
{
	public function __construct(
		private int $id, // ID опроса, для которого мы создаём компонент
		private PollFacade $facade,
	) {
	}

	public function handleVote(int $voteId): void
	{
		$this->facade->vote($this->id, $voteId);
		// ...
	}
}

Если бы мы писали обычный сервис, обсуждать было бы нечего. DI-контейнер незаметно позаботился бы о передаче всех зависимостей. Однако с компонентами мы обычно обходимся созданием нового экземпляра прямо в презентере, в фабричных методах createComponent…(). Но передавать в презентер все зависимости всех компонентов только для того, чтобы передать их дальше компонентам, утомительно. И сколько кода приходится писать…

Логично спросить: почему бы просто не зарегистрировать компонент как обычный сервис, передать его в презентер и затем возвращать в методе createComponent…()? Однако такой подход неуместен, потому что мы хотим иметь возможность при необходимости создавать компонент несколько раз.

Правильное решение – написать для компонента фабрику, то есть класс, который создаёт компонент за нас:

class PollControlFactory
{
	public function __construct(
		private PollFacade $facade,
	) {
	}

	public function create(int $id): PollControl
	{
		return new PollControl($id, $this->facade);
	}
}

Эту фабрику мы регистрируем в нашем контейнере в конфигурации:

services:
	- PollControlFactory

и наконец используем её в своём презентере:

class PollPresenter extends Nette\Application\UI\Presenter
{
	public function __construct(
		private PollControlFactory $pollControlFactory,
	) {
	}

	protected function createComponentPollControl(): PollControl
	{
		$pollId = 1; // мы можем передать свой параметр
		return $this->pollControlFactory->create($pollId);
	}
}

Прекрасно то, что Nette DI умеет порождать такие простые фабрики, поэтому вместо всего её кода вам достаточно написать её интерфейс:

interface PollControlFactory
{
	public function create(int $id): PollControl;
}

И это всё. Nette внутренне реализует этот интерфейс и внедрит его в презентер, где мы сможем им пользоваться. Он волшебным образом добавит в наш компонент параметр $id и экземпляр класса PollFacade.

Компоненты в подробностях

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

  1. Он отрисовывается в шаблоне
  2. Он знает, какую свою часть отрисовать при AJAX-запросе (сниппеты)
  3. Он умеет хранить своё состояние в URL (постоянные параметры)
  4. Он умеет реагировать на действия пользователя (сигналы)
  5. Он образует иерархическую структуру (корнем которой служит презентер)

За каждую из этих функций отвечает один из классов в цепочке наследования. За отрисовку (1 + 2) отвечает Nette\Application\UI\Control, за встраивание в жизненный цикл (3, 4) – класс Nette\Application\UI\Component, а за создание иерархической структуры (5) – классы Container и Component.

Nette\ComponentModel\Component  { IComponent }
|
+- Nette\ComponentModel\Container  { IContainer }
	|
	+- Nette\Application\UI\Component  { SignalReceiver, StatePersistent }
		|
		+- Nette\Application\UI\Control  { Renderable }
			|
			+- Nette\Application\UI\Presenter  { IPresenter }

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

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

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

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

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

class PaginatingControl extends Control
{
	#[Persistent]
	public int $page = 1;

	public function loadState(array $params): void
	{
		parent::loadState($params); // здесь задаётся $this->page
		// далее идёт собственная проверка значения:
		if ($this->page < 1) {
			$this->error();
		}
	}
}

Обратным процессом, то есть сбором значений из постоянных свойств, занимается метод saveState().

Подключение к презентеру

В момент, когда компонент становится частью иерархии презентера, вызываются его callback-функции, хранящиеся в массиве $onAnchor. С этого момента у компонента есть презентер, он может безопасно создавать ссылки, читать постоянные параметры и так далее.

$control->onAnchor[] = function ($control): void {
	// у компонента теперь есть презентер
};

Сигналы в подробностях

Сигнал вызывает перезагрузку страницы ровно так же, как исходный запрос (кроме случая вызова через AJAX), и вызывает метод signalReceived($signal), реализация которого по умолчанию в классе Nette\Application\UI\Component пытается вызвать метод, составленный из слов handle<Signal>. Дальнейшая обработка зависит от объекта. Объекты, наследующие от Component (то есть Control и Presenter), реагируют попыткой вызвать метод handle<Signal> с нужными параметрами.

Иначе говоря: берётся определение функции handle<Signal> вместе со всеми параметрами, пришедшими с запросом, параметры из URL сопоставляются аргументам по имени, и делается попытка вызвать метод. Например, значение параметра id из URL передаётся как аргумент $id, something из URL – как $something и так далее. А если метода не существует, метод signalReceived выбрасывает исключение.

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

Сигнал может принять любой компонент, презентер или объект, который реализует интерфейс SignalReceiver и подключён к дереву компонентов.

Главными получателями сигналов будут Presenters и визуальные компоненты, наследующие от Control. Сигнал предназначен служить объекту знаком, что нужно что-то сделать: опрос должен засчитать голос пользователя, блок новостей должен раскрыться и показать вдвое больше новостей, форма была отправлена и должна обработать данные и так далее.

URL для сигнала создаётся методом Component::link(). Как параметр $destination мы передаём строку {signal}!, а как $args – массив аргументов, которые хотим передать сигналу. Сигнал всегда вызывается на текущем презентере и действии с текущими параметрами; параметры сигнала лишь добавляются. Кроме того, добавляется параметр ?do, задающий сигнал.

Его формат – либо {signal}, либо {signalReceiver}-{signal}. {signalReceiver} – имя компонента в презентере. Поэтому в имени компонента нельзя использовать дефис: он служит для разделения имени компонента и сигнала, хотя таким способом можно вкладывать несколько компонентов друг в друга.

Метод isSignalReceiver() проверяет, является ли компонент (первый аргумент) получателем сигнала (второй аргумент). Второй аргумент можно опустить, тогда проверяется, является ли компонент получателем какого-либо сигнала. Если второй параметр установлен в true, проверяется, является ли получателем указанный компонент или любой из его потомков.

На любом этапе, предшествующем handle<Signal>, мы можем выполнить сигнал вручную, вызвав метод processSignal(), который заботится об обработке сигнала: он берёт компонент, определённый как получатель сигнала (если получатель не указан, это сам презентер), и отправляет ему сигнал.

Пример:

if ($this->isSignalReceiver($this, 'paging') || $this->isSignalReceiver($this, 'sorting')) {
	$this->processSignal();
}

Это выполняет сигнал досрочно, и повторно он вызван не будет.