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`. Но какво да правим, ако искаме да извършим действие само на **текущата страница**? Например, промяна на реда на сортиране на колоните в таблица; изтриване на елемент; превключване на светъл/тъмен режим; изпращане на формуляр; гласуване в анкета и т.н.

Този тип заявка се нарича сигнал. И как действията извикват методи `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()` връща екземпляр на флаш съобщение като 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-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] `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 може да [генерира |dependency-injection:factory] такива прости фабрики, така че вместо да пишете целия код, трябва само да напишете нейния интерфейс:

```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()]. Последователността `{signal}!` се предава като параметър `$destination`, а масивът от аргументи, който искаме да предадем на обработчика на сигнали, се предава като `$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. Но какво да правим, ако искаме да извършим действие само на текущата страница? Например, промяна на реда на сортиране на колоните в таблица; изтриване на елемент; превключване на светъл/тъмен режим; изпращане на формуляр; гласуване в анкета и т.н.

Този тип заявка се нарича сигнал. И как действията извикват методи 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() връща екземпляр на флаш съобщение като stdClass обект, на който може да се предаде информация.

$this->flashMessage('Артикулът е премахнат.');
$this->redirect(/* ... */); // пренасочване

В шаблона тези съобщения са налични в променливата $flashes като обекти stdClass, които съдържат свойства message (текст на съобщението), type (тип на съобщението) и могат да съдържат вече споменатата информация за потребителя. Показваме ги по следния начин:

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

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

Постоянните параметри се използват за поддържане на състоянието на компонентите между различните заявки. Тяхната стойност остава същата дори след щракване върху връзката. За разлика от данните за сесията, те се прехвърлят в 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 безпроблемно ще се погрижи за предаването на всички зависимости. Обикновено обаче работим с компоненти, като създаваме нова тяхна инстанция директно в презентатора чрез фабрични методи 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(). Последователността {signal}! се предава като параметър $destination, а масивът от аргументи, който искаме да предадем на обработчика на сигнали, се предава като $args. Параметрите на сигнала са свързани с URL адреса на текущия водещ/представител. **Параметърът ?do в URL адреса определя сигнала, който ще бъде извикан.

Форматът му е {signal} или {signalReceiver}-{signal}. {signalReceiver} – е името на компонента в презентатора. Ето защо в името на компонента не може да има тире (неточна чертичка) – то се използва за отделяне на името на компонента от сигнала, но могат да се съставят няколко компонента.

Методът isSignalReceiver() проверява дали компонентът (първи аргумент) е приемник на сигнал (втори аргумент). Вторият аргумент може да бъде пропуснат – тогава се установява дали компонентът е приемник на някакъв сигнал. Ако вторият аргумент е true, се проверява дали компонентът или неговите наследници са приемници на сигнали.

Във всяка фаза преди handle{Signal}, сигналът може да бъде изпълнен ръчно чрез извикване на метода processSignal(), който поема отговорността за изпълнението на сигнала. Той приема компонента на приемника (ако не е зададен, това е самият водещ) и му изпраща сигнала.

Пример:

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

Сигналът е изпълнен преждевременно и няма да бъде извикан отново.