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, і просто вставити в презентер.

Ви можете включити в презентер будь-яку кількість компонентів. І ви можете вставляти інші компоненти в деякі компоненти. Це створює дерево компонентів із презентером як коренем.


Фабричні методи .[#toc-factory-methods]
=======================================

Як розміщуються і згодом використовуються компоненти в презентері? Зазвичай з використанням фабричних методів.

Фабрика компонентів - це елегантний спосіб створювати компоненти тільки тоді, коли вони справді потрібні (ліньки / на вимогу). Уся магія полягає в реалізації методу `createComponent<Name>()`, де `<Name>` - ім'я компонента, який буде створено та повернуто.

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

Оскільки всі компоненти створюються в окремих методах, код чистіший і легше читається.

.[note]
Імена компонентів завжди починаються з малої літери, хоча в імені методу вони пишуться із великої.

Ми ніколи не викликаємо фабрики безпосередньо, вони викликаються автоматично, коли ми вперше використовуємо компоненти. Завдяки цьому компонент створюється в потрібний момент, і тільки якщо він дійсно необхідний. Якщо ми не використовуватимемо компонент (наприклад, під час AJAX-запиту, коли ми повертаємо тільки частину сторінки, або коли частини кешуються), він навіть не буде створений, і ми заощадимо продуктивність сервера.

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

У шаблоні ви можете візуалізувати компонент за допомогою тега [{control} |#Rendering]. Тому немає необхідності вручну передавати компоненти в шаблон.

```latte
<h2>Проголосуйте, пожалуйста</h2>

{control poll}
```


Голлівудський стиль .[#toc-hollywood-style]
===========================================

Компоненти зазвичай використовують один класний прийом, який ми любимо називати голлівудським стилем. Напевно ви знаєте це кліше, яке актори часто чують на кастингах: "Не телефонуйте нам, ми зателефонуємо вам". І це те, про що йдеться.

У Nette, замість того, щоб постійно ставити запитання ("чи була форма надіслана?", "чи була вона дійсною?", чи "чи натиснув хто-небудь на цю кнопку?"), ви кажете фреймворку "коли це станеться, викличте цей метод" і залиште подальшу роботу над ним. Якщо ви програмуєте на JavaScript, ви знайомі з цим стилем програмування. Ви пишете функції, які викликаються при настанні певної події. А движок передає їм відповідні параметри.

Це повністю змінює спосіб написання додатків. Що більше завдань ви можете делегувати фреймворку, то менше у вас роботи. І тим менше ви можете забути.


Як написати компонент .[#toc-how-to-write-a-component]
======================================================

Під компонентом ми зазвичай маємо на увазі нащадків класу [api:Nette\Application\UI\Control]. Сам презентер [api:Nette\Application\UI\Presenter] також є нащадком класу `Control`.

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

class PollControl extends Control
{
}
```


Рендеринг .[#toc-rendering]
===========================

Ми вже знаємо, що тег `{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 шлях до кореневого каталогу (наприклад, `/CD-collection`).
- `$baseUrl` - абсолютний URL до кореневого каталогу (наприклад, `http://localhost/CD-collection`)
- `$user` - це об'єкт, [що представляє користувача |security:authentication].
- `$presenter` - поточний презентер
- `$control` - поточний компонент
- `$flashes` - список [повідомлень |#flash-сообщений], надісланих методом `flashMessage()`.


Сигнал .[#toc-signal]
=====================

Ми вже знаємо, що навігація в додатку Nette складається з посилань або перенаправлення на пари `Presenter:action`. Але що якщо ми просто хочемо виконати дію на **поточній сторінці**? Наприклад, змінити порядок сортування стовпців у таблиці; видалити елемент; перемкнути режим light/dark; надіслати форму; проголосувати в опитуванні; тощо.

Такий тип запиту називається сигналом. І як дії викликають методи `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#Links-to-Signal].

```latte
<a n:href="click! $x, $y">нажмите сюда</a>
```

Сигнал завжди викликається на поточному презентері та поданні, тому неможливо пов'язати сигнал з іншим презентером/дією.

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


Сніпети та AJAX .[#toc-snippets-and-ajax]
=========================================

Сигнали можуть трохи нагадати вам AJAX: обробники, які викликаються на поточній сторінці. І ви маєте рацію, сигнали дійсно часто викликаються за допомогою AJAX, і тоді ми передаємо браузеру тільки змінені частини сторінки. Вони називаються сніпетами. Більш детальну інформацію можна знайти на [сторінці про AJAX |ajax].


Флеш-повідомлення .[#toc-flash-messages]
========================================

Компонент має власне сховище флеш-повідомлень, яке не залежить від презентера. Це повідомлення, які, наприклад, інформують про результат операції. Важливою особливістю флеш-повідомлень є те, що вони доступні в шаблоні навіть після перенаправлення. Навіть після відображення вони залишатимуться живими ще 30 секунд - наприклад, на випадок, якщо користувач ненавмисно оновить сторінку, повідомлення не буде втрачено.

Надсилання здійснюється методом [flashMessage |api:Nette\Application\UI\Control::flashMessage()]. Першим параметром є текст повідомлення або об'єкт `stdClass`, що представляє повідомлення. Необов'язковий другий параметр - це його тип (помилка, попередження, інформація тощо). Метод `flashMessage()` повертає екземпляр flash-повідомлення як об'єкт stdClass, якому можна передати інформацію.

```php
$this->flashMessage('Елемент було видалено.');
$this->redirect(/* ... */); // робимо редирект
```

У шаблоні ці повідомлення доступні у змінній `$flashes` як об'єкти `stdClass`, які містять властивості `message` (текст повідомлення), `type` (тип повідомлення) і можуть містити вже згадану інформацію про користувача. Ми відображаємо їх таким чином:

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


Перенаправлення за сигналом .[#toc-redirection-after-a-signal]
==============================================================

Після обробки сигналу компонента часто відбувається перенаправлення. Ця ситуація схожа на форми - після відправлення форми ми також виконуємо перенаправлення, щоб запобігти повторному відправленню даних при оновленні сторінки в браузері.

```php
$this->redirect('this') // redirects to the current presenter and action
```

Оскільки компонент є елементом багаторазового використання і зазвичай не повинен мати прямої залежності від конкретних доповідачів, методи `redirect()` і `link()` автоматично інтерпретують параметр як сигнал компонента:

```php
$this->redirect('click') // redirects to the 'click' signal of the same component
```

Якщо вам потрібно перенаправити на іншого доповідача або дію, ви можете зробити це через доповідача:

```php
$this->getPresenter()->redirect('Product:show'); // redirects to a different presenter/action
```


Постійні параметри .[#toc-persistent-parameters]
================================================

Постійні параметри використовуються для збереження стану компонентів між різними запитами. Їх значення залишається незмінним навіть після переходу за посиланням. На відміну від сесійних даних, вони передаються в URL-адресі. І вони передаються автоматично, включаючи посилання, створені в інших компонентах на тій же сторінці.

Наприклад, у вас є компонент підкачки контенту. Таких компонентів на сторінці може бути декілька. І ви хочете, щоб при переході за посиланням всі компоненти залишалися на своїй поточній сторінці. Тому ми робимо номер сторінки (`page`) постійним параметром.

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

```php
use Nette\Application\Attributes\Persistent; // цей рядок важливий

class PaginatingControl extends Control
{
	#[Persistent]
	public int $page = 1; // повинні бути публічними
}
```

Ми рекомендуємо вам вказати тип даних (наприклад, `int`) разом з властивістю, а також ви можете вказати значення за замовчуванням. Значення параметрів можуть бути [перевірені |#Validation of Persistent Parameters].

Ви можете змінити значення постійного параметра під час створення посилання:

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

Або ж його можна *скинути*, тобто видалити з URL-адреси. Тоді він прийме значення за замовчуванням:

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


Постійні компоненти .[#toc-persistent-components]
=================================================

Постійними можуть бути не тільки параметри, а й компоненти. Їхні постійні параметри також передаються між різними діями або між різними презентерами. Ми позначаємо постійні компоненти цією анотацією для класу презентера. Наприклад, тут ми позначаємо компоненти `calendar` і `poll` таким чином:

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

Вам не потрібно позначати підкомпоненти як постійні, вони стають постійними автоматично.

У PHP 8 ви також можете використовувати атрибути для маркування постійних компонентів:

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

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


Компоненти із залежностями .[#toc-components-with-dependencies]
===============================================================

Як створити компоненти із залежностями, не "заплутавши" ведучих, які будуть їх використовувати? Завдяки продуманим можливостям 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($id, $voteId);
		// ...
	}
}
```

Якби ми писали класичний сервіс, то турбуватися було б нема про що. Контейнер DI непомітно подбав би про передачу всіх залежностей. Але ми зазвичай працюємо з компонентами, створюючи їхній новий екземпляр безпосередньо в презентері в [factory methods |#factory methods] `createComponent...()`. Але передача всіх залежностей усіх компонентів у презентер, щоб потім передати їх компонентам, громіздка. І кількість написаного коду...

Логічне запитання: чому б нам просто не зареєструвати компонент як класичний сервіс, передати його ведучому, а потім повернути його в методі `createComponent...()`? Але такий підхід недоречний, оскільки ми хочемо мати можливість створювати компонент багаторазово.

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

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

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

Тепер ми реєструємо наш сервіс у DI-контейнері для конфігурації:

```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-контейнер згенерує реалізацію:

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

Ось і все. Nette внутрішньо реалізує цей інтерфейс і передає його нашому презентеру, де ми можемо його використовувати. Він також магічним чином передає наш параметр `$id` і екземпляр класу `PollFacade` у наш компонент.


Компоненти в глибину .[#toc-components-in-depth]
================================================

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

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

Кожна з цих функцій обробляється одним із класів лінії успадкування. Рендеринг (1 + 2) обробляється [api:Nette\Application\UI\Control], включення в [життєвий цикл |presenters#life-cycle-of-presenter] (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 }
```


Життєвий цикл компонента .[#toc-life-cycle-of-component]
--------------------------------------------------------

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


Перевірка постійних параметрів .[#toc-validation-of-persistent-parameters]
--------------------------------------------------------------------------

Значення [постійних параметрів |#persistent parameters], отримані з 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()`.


Сигнали в глибину .[#toc-signals-in-depth]
------------------------------------------

Сигнал викликає перезавантаження сторінки подібно до вихідного запиту (за винятком AJAX) і викликає метод `signalReceived($signal)`, реалізація якого за замовчуванням у класі `Nette\Application\UI\Component` намагається викликати метод, що складається зі слів `handle{Signal}`. Подальша обробка залежить від цього об'єкта. Об'єкти, які є нащадками `Component` (тобто `Control` і `Presenter`), намагаються викликати `handle{Signal}` з відповідними параметрами.

Іншими словами: береться визначення методу `handle{Signal}` і всі параметри, які були отримані в запиті, зіставляються з параметрами методу. Це означає, що параметр `id` з URL зіставляється з параметром методу `$id`, `something` - з `$something` і так далі. А якщо метод не існує, то метод `signalReceived` викидає [виняток |api:Nette\Application\UI\BadSignalException].

Сигнал може бути отриманий будь-яким компонентом, провідним об'єктом, що реалізує інтерфейс `SignalReceiver`, якщо він підключений до дерева компонентів.

Основними одержувачами сигналів є презентери та візуальні компоненти, що розширюють `Control`. Сигнал - це знак для об'єкта, що він має щось зробити - опитування зараховує голос користувача, скринька з новинами має розгорнутися, форму було відправлено, і вона має обробити дані тощо.

URL для сигналу створюється за допомогою методу [Component::link() |api:Nette\Application\UI\Component::link()]. Як параметр `$destination` передається рядок `{signal}!`, а як `$args` - масив аргументів, які ми хочемо передати обробнику сигналу. Параметри сигналу прив'язуються до URL поточного презентера/представлення. **Параметр `?do` в URL визначає сигнал, що викликається.**

Його формат - `{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->item;
		return $poll;
	}
}

Оскільки всі компоненти створюються в окремих методах, код чистіший і легше читається.

Імена компонентів завжди починаються з малої літери, хоча в імені методу вони пишуться із великої.

Ми ніколи не викликаємо фабрики безпосередньо, вони викликаються автоматично, коли ми вперше використовуємо компоненти. Завдяки цьому компонент створюється в потрібний момент, і тільки якщо він дійсно необхідний. Якщо ми не використовуватимемо компонент (наприклад, під час AJAX-запиту, коли ми повертаємо тільки частину сторінки, або коли частини кешуються), він навіть не буде створений, і ми заощадимо продуктивність сервера.

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

У шаблоні ви можете візуалізувати компонент за допомогою тега {control}. Тому немає необхідності вручну передавати компоненти в шаблон.

<h2>Проголосуйте, пожалуйста</h2>

{control poll}

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

Компоненти зазвичай використовують один класний прийом, який ми любимо називати голлівудським стилем. Напевно ви знаєте це кліше, яке актори часто чують на кастингах: „Не телефонуйте нам, ми зателефонуємо вам“. І це те, про що йдеться.

У Nette, замість того, щоб постійно ставити запитання („чи була форма надіслана?“, „чи була вона дійсною?“, чи „чи натиснув хто-небудь на цю кнопку?“), ви кажете фреймворку „коли це станеться, викличте цей метод“ і залиште подальшу роботу над ним. Якщо ви програмуєте на JavaScript, ви знайомі з цим стилем програмування. Ви пишете функції, які викликаються при настанні певної події. А движок передає їм відповідні параметри.

Це повністю змінює спосіб написання додатків. Що більше завдань ви можете делегувати фреймворку, то менше у вас роботи. І тим менше ви можете забути.

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

Під компонентом ми зазвичай маємо на увазі нащадків класу Nette\Application\UI\Control. Сам презентер 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 шлях до кореневого каталогу (наприклад, /CD-collection).
  • $baseUrl – абсолютний URL до кореневого каталогу (наприклад, http://localhost/CD-collection)
  • $user – це об'єкт, що представляє користувача.
  • $presenter – поточний презентер
  • $control – поточний компонент
  • $flashes – список повідомлень, надісланих методом flashMessage().

Сигнал

Ми вже знаємо, що навігація в додатку Nette складається з посилань або перенаправлення на пари Presenter:action. Але що якщо ми просто хочемо виконати дію на поточній сторінці? Наприклад, змінити порядок сортування стовпців у таблиці; видалити елемент; перемкнути режим light/dark; надіслати форму; проголосувати в опитуванні; тощо.

Такий тип запиту називається сигналом. І як дії викликають методи 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">нажмите сюда</a>

Сигнал завжди викликається на поточному презентері та поданні, тому неможливо пов'язати сигнал з іншим презентером/дією.

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

Сніпети та AJAX

Сигнали можуть трохи нагадати вам AJAX: обробники, які викликаються на поточній сторінці. І ви маєте рацію, сигнали дійсно часто викликаються за допомогою AJAX, і тоді ми передаємо браузеру тільки змінені частини сторінки. Вони називаються сніпетами. Більш детальну інформацію можна знайти на сторінці про AJAX.

Флеш-повідомлення

Компонент має власне сховище флеш-повідомлень, яке не залежить від презентера. Це повідомлення, які, наприклад, інформують про результат операції. Важливою особливістю флеш-повідомлень є те, що вони доступні в шаблоні навіть після перенаправлення. Навіть після відображення вони залишатимуться живими ще 30 секунд – наприклад, на випадок, якщо користувач ненавмисно оновить сторінку, повідомлення не буде втрачено.

Надсилання здійснюється методом flashMessage. Першим параметром є текст повідомлення або об'єкт stdClass, що представляє повідомлення. Необов'язковий другий параметр – це його тип (помилка, попередження, інформація тощо). Метод flashMessage() повертає екземпляр flash-повідомлення як об'єкт stdClass, якому можна передати інформацію.

$this->flashMessage('Елемент було видалено.');
$this->redirect(/* ... */); // робимо редирект

У шаблоні ці повідомлення доступні у змінній $flashes як об'єкти stdClass, які містять властивості message (текст повідомлення), type (тип повідомлення) і можуть містити вже згадану інформацію про користувача. Ми відображаємо їх таким чином:

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

Перенаправлення за сигналом

Після обробки сигналу компонента часто відбувається перенаправлення. Ця ситуація схожа на форми – після відправлення форми ми також виконуємо перенаправлення, щоб запобігти повторному відправленню даних при оновленні сторінки в браузері.

$this->redirect('this') // redirects to the current presenter and action

Оскільки компонент є елементом багаторазового використання і зазвичай не повинен мати прямої залежності від конкретних доповідачів, методи redirect() і link() автоматично інтерпретують параметр як сигнал компонента:

$this->redirect('click') // redirects to the 'click' signal of the same component

Якщо вам потрібно перенаправити на іншого доповідача або дію, ви можете зробити це через доповідача:

$this->getPresenter()->redirect('Product:show'); // redirects to a different presenter/action

Постійні параметри

Постійні параметри використовуються для збереження стану компонентів між різними запитами. Їх значення залишається незмінним навіть після переходу за посиланням. На відміну від сесійних даних, вони передаються в URL-адресі. І вони передаються автоматично, включаючи посилання, створені в інших компонентах на тій же сторінці.

Наприклад, у вас є компонент підкачки контенту. Таких компонентів на сторінці може бути декілька. І ви хочете, щоб при переході за посиланням всі компоненти залишалися на своїй поточній сторінці. Тому ми робимо номер сторінки (page) постійним параметром.

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

use Nette\Application\Attributes\Persistent; // цей рядок важливий

class PaginatingControl extends Control
{
	#[Persistent]
	public int $page = 1; // повинні бути публічними
}

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

Ви можете змінити значення постійного параметра під час створення посилання:

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

Або ж його можна скинути, тобто видалити з URL-адреси. Тоді він прийме значення за замовчуванням:

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

Постійні компоненти

Постійними можуть бути не тільки параметри, а й компоненти. Їхні постійні параметри також передаються між різними діями або між різними презентерами. Ми позначаємо постійні компоненти цією анотацією для класу презентера. Наприклад, тут ми позначаємо компоненти calendar і poll таким чином:

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

Вам не потрібно позначати підкомпоненти як постійні, вони стають постійними автоматично.

У PHP 8 ви також можете використовувати атрибути для маркування постійних компонентів:

use Nette\Application\Attributes\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($id, $voteId);
		// ...
	}
}

Якби ми писали класичний сервіс, то турбуватися було б нема про що. Контейнер DI непомітно подбав би про передачу всіх залежностей. Але ми зазвичай працюємо з компонентами, створюючи їхній новий екземпляр безпосередньо в презентері в factory methods createComponent...(). Але передача всіх залежностей усіх компонентів у презентер, щоб потім передати їх компонентам, громіздка. І кількість написаного коду…

Логічне запитання: чому б нам просто не зареєструвати компонент як класичний сервіс, передати його ведучому, а потім повернути його в методі createComponent...()? Але такий підхід недоречний, оскільки ми хочемо мати можливість створювати компонент багаторазово.

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

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

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

Тепер ми реєструємо наш сервіс у DI-контейнері для конфігурації:

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().

Сигнали в глибину

Сигнал викликає перезавантаження сторінки подібно до вихідного запиту (за винятком AJAX) і викликає метод signalReceived($signal), реалізація якого за замовчуванням у класі Nette\Application\UI\Component намагається викликати метод, що складається зі слів handle{Signal}. Подальша обробка залежить від цього об'єкта. Об'єкти, які є нащадками Component (тобто Control і Presenter), намагаються викликати handle{Signal} з відповідними параметрами.

Іншими словами: береться визначення методу handle{Signal} і всі параметри, які були отримані в запиті, зіставляються з параметрами методу. Це означає, що параметр id з URL зіставляється з параметром методу $id, something – з $something і так далі. А якщо метод не існує, то метод signalReceived викидає виняток.

Сигнал може бути отриманий будь-яким компонентом, провідним об'єктом, що реалізує інтерфейс SignalReceiver, якщо він підключений до дерева компонентів.

Основними одержувачами сигналів є презентери та візуальні компоненти, що розширюють Control. Сигнал – це знак для об'єкта, що він має щось зробити – опитування зараховує голос користувача, скринька з новинами має розгорнутися, форму було відправлено, і вона має обробити дані тощо.

URL для сигналу створюється за допомогою методу Component::link(). Як параметр $destination передається рядок {signal}!, а як $args – масив аргументів, які ми хочемо передати обробнику сигналу. Параметри сигналу прив'язуються до URL поточного презентера/представлення. Параметр ?do в URL визначає сигнал, що викликається.

Його формат – {signal} або {signalReceiver}-{signal}. {signalReceiver} – це ім'я компонента в презентері. Саме тому дефіс (неточно тире) не може бути присутнім в імені компонентів – він використовується для розділення імені компонента і сигналу, але можна скласти кілька компонентів.

Метод isSignalReceiver() перевіряє, чи є компонент (перший аргумент) приймачем сигналу (другий аргумент). Другий аргумент може бути опущений – тоді з'ясовується, чи є компонент приймачем будь-якого сигналу. Якщо другий параметр дорівнює true, то перевіряється, чи є компонент або його нащадки приймачами сигналу.

У будь-якій фазі, що передує handle{Signal}, сигнал можна виконати вручну, викликавши метод processSignal(), який бере на себе відповідальність за виконання сигналу. Приймає компонент-приймач (якщо він не встановлений, то це сам презентер) і посилає йому сигнал.

Приклад:

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

Сигнал виконується передчасно і більше не буде викликаний.