-
-
Notifications
You must be signed in to change notification settings - Fork 282
/
components.texy
463 lines (321 loc) · 32.6 KB
/
components.texy
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
Интерактивни компоненти
***********************
<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();
}
```
Сигналът е изпълнен преждевременно и няма да бъде извикан отново.