-
-
Notifications
You must be signed in to change notification settings - Fork 277
Expand file tree
/
Copy pathcomponents.texy
More file actions
485 lines (335 loc) · 32.4 KB
/
components.texy
File metadata and controls
485 lines (335 loc) · 32.4 KB
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
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
Інтерактивні компоненти
***********************
<div class=perex>
Компоненти — це окремі об'єкти, що використовуються повторно, які ми вставляємо на сторінки. Це можуть бути форми, таблиці даних, опитування, власне все, що має сенс використовувати повторно. Ми покажемо:
- як використовувати компоненти?
- як їх писати?
- що таке сигнали?
</div>
Nette має вбудовану систему компонентів. Щось подібне можуть пам'ятати ті, хто працював з Delphi або ASP.NET Web Forms, на чомусь віддалено схожому побудовані React або Vue.js. Однак у світі PHP-фреймворків це унікальна річ.
При цьому компоненти суттєво впливають на підхід до створення застосунків. Ви можете складати сторінки з готових блоків. Потрібна таблиця даних в адміністративній панелі? Знайдіть її на [Componette |https://componette.org/search/component], репозиторії доповнень з відкритим кодом (тобто не тільки компонентів) для Nette, і просто вставте в presenter.
До presenter'а можна включити будь-яку кількість компонентів. А в деякі компоненти можна вставляти інші компоненти. Таким чином створюється дерево компонентів, коренем якого є presenter.
Фабричні методи
===============
Як компоненти вставляються в presenter і потім використовуються? Зазвичай за допомогою фабричних методів.
Фабрика компонентів — це елегантний спосіб створювати компоненти лише тоді, коли вони дійсно потрібні (lazy / on demand). Вся магія полягає в реалізації методу з назвою `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} |#Відображення]. Тому не потрібно вручну передавати компоненти в шаблон.
```latte
<h2>Голосуйте</h2>
{control poll}
```
Голлівудський стиль
===================
Компоненти зазвичай використовують одну свіжу техніку, яку ми любимо називати Голлівудським стилем. Ви напевно знаєте крилату фразу, яку так часто чують учасники кінопроб: "Не дзвоніть нам, ми вам зателефонуємо". Саме про це йдеться.
У Nette замість того, щоб постійно щось запитувати ("чи була надіслана форма?", "чи була вона валідною?" або "чи натиснув користувач цю кнопку?"), ви кажете фреймворку "коли це станеться, виклич цей метод" і залишаєте подальшу роботу йому. Якщо ви програмуєте на JavaScript, цей стиль програмування вам добре знайомий. Ви пишете функції, які викликаються, коли настає певна подія. І мова передає їм відповідні параметри.
Це повністю змінює погляд на написання застосунків. Чим більше завдань ви можете залишити фреймворку, тим менше роботи у вас. І тим менше ви можете щось пропустити.
Пишемо компонент
================
Під поняттям компонент зазвичай мається на увазі нащадок класу [api:Nette\Application\UI\Control]. (Точніше було б використовувати термін "controls", але "контроли" мають в українській мові зовсім інше значення, і скоріше прижилися "компоненти".) Сам presenter [api:Nette\Application\UI\Presenter] є, до речі, також нащадком класу `Control`.
```php .{file:PollControl.php}
use Nette\Application\UI\Control;
class PollControl extends Control
{
}
```
Відображення
============
Ми вже знаємо, що для відображення компонента використовується тег `{control componentName}`. Він фактично викликає метод `render()` компонента, в якому ми дбаємо про відображення. У нас є, так само як і в presenter'і, [Latte шаблон|templates] у змінній `$this->template`, куди ми передаємо параметри. На відміну від presenter'а, ми повинні вказати файл із шаблоном і змусити його відобразитися:
```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();
```
Компоненти, так само як і presenter'и, автоматично передають у шаблони кілька корисних змінних:
- `$basePath` — абсолютний URL-шлях до кореневого каталогу (наприклад, `/eshop`)
- `$baseUrl` — абсолютний URL до кореневого каталогу (наприклад, `http://localhost/eshop`)
- `$user` — об'єкт [що представляє користувача |security:authentication]
- `$presenter` — поточний presenter
- `$control` — поточний компонент
- `$flashes` — масив [повідомлень |#Flash-повідомлення], надісланих функцією `flashMessage()`
Сигнал
======
Ми вже знаємо, що навігація в застосунку Nette полягає у посиланні або перенаправленні на пари `Presenter:action`. Але що, якщо ми просто хочемо виконати дію на **поточній сторінці**? Наприклад, змінити сортування стовпців у таблиці; видалити елемент; перемкнути світлий/темний режим; надіслати форму; проголосувати в опитуванні тощо.
Цей тип запитів називається сигналами. І подібно до того, як дії викликають методи `action<Action>()` або `render<Action>()`, сигнали викликають методи `handle<Signal>()`. У той час як поняття дії (або view) пов'язане виключно з presenter'ами, сигнали стосуються всіх компонентів. А отже, й presenter'ів, оскільки `UI\Presenter` є нащадком `UI\Control`.
```php
public function handleClick(int $x, int $y): void
{
// ... обробка сигналу ...
}
```
Посилання, що викликає сигнал, створюється звичайним способом, тобто в шаблоні атрибутом `n:href` або тегом `{link}`, у коді методом `link()`. Більше в розділі [Створення URL-посилань |creating-links#Посилання на сигнал].
```latte
<a n:href="click! $x, $y">натисніть тут</a>
```
Сигнал завжди викликається на поточному presenter'і та action, його неможливо викликати на іншому presenter'і або іншому action.
Сигнал, отже, спричиняє перезавантаження сторінки так само, як і при початковому запиті, лише додатково викликає метод обробки сигналу з відповідними параметрами. Якщо метод не існує, викидається виняток [api:Nette\Application\UI\BadSignalException], який користувачеві відображається як сторінка помилки 403 Forbidden.
Сніпети та AJAX
===============
Сигнали вам, можливо, трохи нагадують AJAX: обробники, які викликаються на поточній сторінці. І ви маєте рацію, сигнали дійсно часто викликаються за допомогою AJAX, і потім ми передаємо в браузер лише змінені частини сторінки. Тобто так звані сніпети. Більше інформації ви знайдете на [сторінці, присвяченій AJAX |ajax].
Flash-повідомлення
==================
Компонент має власне сховище flash-повідомлень, незалежне від presenter'а. Це повідомлення, які, наприклад, інформують про результат операції. Важливою особливістю flash-повідомлень є те, що вони доступні в шаблоні навіть після перенаправлення. Навіть після відображення вони залишаються активними ще 30 секунд – наприклад, на випадок, якщо через помилку передачі користувач оновить сторінку - повідомлення йому одразу не зникне.
Надсилання забезпечує метод [flashMessage |api:Nette\Application\UI\Control::flashMessage()]. Першим параметром є текст повідомлення або об'єкт `stdClass`, що представляє повідомлення. Необов'язковим другим параметром є його тип (error, warning, info тощо). Метод `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}
```
Перенаправлення після сигналу
=============================
Після обробки сигналу компонента часто відбувається перенаправлення. Це схожа ситуація, як з формами - після їх надсилання ми також перенаправляємо, щоб при оновленні сторінки в браузері не відбулося повторного надсилання даних.
```php
$this->redirect('this') // перенаправляє на поточний presenter та action
```
Оскільки компонент є елементом, що використовується повторно, і зазвичай не повинен мати прямого зв'язку з конкретними presenter'ами, методи `redirect()` та `link()` автоматично інтерпретують параметр як сигнал компонента:
```php
$this->redirect('click') // перенаправляє на сигнал 'click' того ж компонента
```
Якщо вам потрібно перенаправити на інший presenter чи дію, ви можете зробити це через presenter:
```php
$this->getPresenter()->redirect('Product:show'); // перенаправляє на інший presenter/action
```
Персистентні параметри
======================
Персистентні параметри служать для підтримки стану в компонентах між різними запитами. Їхнє значення залишається незмінним навіть після натискання на посилання. На відміну від даних у сесії, вони передаються в URL. І це відбувається повністю автоматично, включно з посиланнями, створеними в інших компонентах на тій самій сторінці.
Наприклад, у вас є компонент для пагінації вмісту. Таких компонентів на сторінці може бути кілька. І ми хочемо, щоб після натискання на посилання всі компоненти залишалися на своїй поточній сторінці. Тому ми зробимо номер сторінки (`page`) персистентним параметром.
Створення персистентного параметра в Nette надзвичайно просте. Достатньо створити публічну властивість і позначити її атрибутом: (раніше використовувалося `/** @persistent */`)
```php
use Nette\Application\Attributes\Persistent; // цей рядок важливий
class PaginatingControl extends Control
{
#[Persistent]
public int $page = 1; // має бути public
}
```
Для властивості рекомендуємо вказувати тип даних (наприклад, `int`) і ви можете вказати значення за замовчуванням. Значення параметрів можна [валідувати |#Валідація персистентних параметрів].
При створенні посилання можна змінити значення персистентного параметра:
```latte
<a n:href="this page: $page + 1">наступна</a>
```
Або його можна *скинути*, тобто видалити з URL. Тоді він набуде свого значення за замовчуванням:
```latte
<a n:href="this page: null">скинути</a>
```
Персистентні компоненти
=======================
Не тільки параметри, але й компоненти можуть бути персистентними. У такого компонента його персистентні параметри передаються і між різними діями presenter'а, або між кількома presenter'ами. Персистентні компоненти позначаємо анотацією біля класу presenter'а. Наприклад, так позначимо компоненти `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
{
}
```
Компоненти із залежностями
==========================
Як створювати компоненти із залежностями, не "забруднюючи" presenter'ів, які їх використовуватимуть? Завдяки розумним властивостям DI-контейнера в Nette можна, так само як при використанні класичних сервісів, залишити більшу частину роботи фреймворку.
Візьмемо як приклад компонент, який має залежність від сервісу `PollFacade`:
```php
class PollControl extends Control
{
public function __construct(
private int $id, // Id опитування, для якого ми створюємо компонент
private PollFacade $facade,
) {
}
public function handleVote(int $voteId): void
{
$this->facade->vote($this->id, $voteId);
// ...
}
}
```
Якби ми писали класичний сервіс, не було б чого вирішувати. Про передачу всіх залежностей невидимо подбав би DI-контейнер. Але з компонентами ми зазвичай поводимося так, що їхній новий екземпляр створюємо безпосередньо в presenter'і в [фабричних методах |#Фабричні методи] `createComponent…()`. Але передавати всі залежності всіх компонентів у presenter, щоб потім передати їх компонентам, незручно. І стільки написаного коду…
Логічним питанням є, чому б просто не зареєструвати компонент як класичний сервіс, не передати його в presenter і потім у методі `createComponent…()` не повертати? Такий підхід, однак, недоречний, оскільки ми хочемо мати можливість створювати компонент навіть кілька разів.
Правильним рішенням є написати для компонента фабрику, тобто клас, який нам створить компонент:
```php
class PollControlFactory
{
public function __construct(
private PollFacade $facade,
) {
}
public function create(int $id): PollControl
{
return new PollControl($id, $this->facade);
}
}
```
Таким чином, фабрику зареєструємо в нашому контейнері в конфігурації:
```neon
services:
- PollControlFactory
```
і нарешті використаємо її в нашому presenter'і:
```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 внутрішньо реалізує цей інтерфейс і передасть його в presenter, де ми вже можемо його використовувати. Магічно він додасть до нашого компонента і параметр `$id`, і екземпляр класу `PollFacade`.
Компоненти до глибини
=====================
Компоненти в Nette Application представляють собою повторно використовувані частини веб-застосунку, які ми вставляємо на сторінки і яким, власне, присвячена вся ця глава. Які саме можливості має такий компонент?
1) його можна відобразити в шаблоні
2) він знає, [яку свою частину |ajax#Сніпети] має відобразити при AJAX-запиті (сніпети)
3) він має можливість зберігати свій стан в URL (персистентні параметри)
4) він має можливість реагувати на дії користувача (сигнали)
5) він створює ієрархічну структуру (де коренем є presenter)
Кожну з цих функцій забезпечує певний клас спадкової лінії. За відображення (1 + 2) відповідає [api:Nette\Application\UI\Control], за включення в [життєвий цикл |presenters#Життєвий цикл презентера] (3, 4) — клас [api:Nette\Application\UI\Component], а за створення ієрархічної структури (5) — класи [Container та Component |component-model:].
```
Nette\ComponentModel\Component { IComponent }
|
+- Nette\ComponentModel\Container { IContainer }
|
+- Nette\Application\UI\Component { SignalReceiver, StatePersistent }
|
+- Nette\Application\UI\Control { Renderable }
|
+- Nette\Application\UI\Presenter { IPresenter }
```
Життєвий цикл компонента
------------------------
[* lifecycle-component.svg *] *** *Життєвий цикл компонента* .<>
Валідація персистентних параметрів
----------------------------------
Значення [персистентних параметрів |#Персистентні параметри], отримані з URL, записує у властивості метод `loadState()`. Він також перевіряє, чи відповідає тип даних, вказаний у властивості, інакше відповідає помилкою 404 і сторінка не відображається.
Ніколи сліпо не довіряйте персистентним параметрам, оскільки їх може легко перезаписати користувач в URL. Таким чином, наприклад, перевіримо, чи номер сторінки `$this->page` більший за 0. Підходящим способом є перезапис згаданого методу `loadState()`:
```php
class PaginatingControl extends Control
{
#[Persistent]
public int $page = 1;
public function loadState(array $params): void
{
parent::loadState($params); // тут встановлюється $this->page
// далі йде власна перевірка значення:
if ($this->page < 1) {
$this->error();
}
}
}
```
Зворотний процес, тобто збір значень з персистентних властивостей, відповідає метод `saveState()`.
Сигнали до глибини
------------------
Сигнал спричиняє перезавантаження сторінки так само, як і при початковому запиті (крім випадку, коли він викликаний AJAX) і викликає метод `signalReceived($signal)`, стандартна реалізація якого в класі `Nette\Application\UI\Component` намагається викликати метод, складений зі слів `handle{signal}`. Подальша обробка залежить від конкретного об'єкта. Об'єкти, що успадковують від `Component` (тобто `Control` і `Presenter`), реагують так, що намагаються викликати метод `handle{signal}` з відповідними параметрами.
Іншими словами: береться визначення функції `handle{signal}` та всі параметри, що прийшли із запитом, і до аргументів за іменем підставляються параметри з URL, і намагається викликати даний метод. Наприклад, як параметр `$id` передається значення з параметра `id` в URL, як `$something` передається `something` з URL тощо. І якщо метод не існує, метод `signalReceived` викидає [виняток |api:Nette\Application\UI\BadSignalException].
Сигнал може приймати будь-який компонент, presenter або об'єкт, який реалізує інтерфейс `SignalReceiver` і підключений до дерева компонентів.
Основними одержувачами сигналів будуть `Presenter`'и та візуальні компоненти, що успадковують від `Control`. Сигнал має служити знаком для об'єкта, що він має щось зробити – опитування має зарахувати голос від користувача, блок з новинами має розгорнутися і показати вдвічі більше новин, форма була надіслана і має обробити дані тощо.
URL для сигналу створюємо за допомогою методу [Component::link() |api:Nette\Application\UI\Component::link()]. Як параметр `$destination` передаємо рядок `{signal}!` і як `$args` масив аргументів, які ми хочемо передати сигналу. Сигнал завжди викликається на поточному presenter'і та action з поточними параметрами, параметри сигналу лише додаються. Крім того, на самому початку додається **параметр `?do`, який визначає сигнал**.
Його формат — або `{signal}`, або `{signalReceiver}-{signal}`. `{signalReceiver}` — це назва компонента в presenter'і. Тому в назві компонента не може бути дефіса — він використовується для розділення назви компонента і сигналу, однак таким чином можна вкладати кілька компонентів.
Метод [isSignalReceiver()|api:Nette\Application\UI\Presenter::isSignalReceiver()] перевіряє, чи є компонент (перший аргумент) одержувачем сигналу (другий аргумент). Другий аргумент можна опустити — тоді він з'ясовує, чи є компонент одержувачем будь-якого сигналу. Як другий параметр можна вказати `true`, і цим перевірити, чи є одержувачем не тільки вказаний компонент, але й будь-який його нащадок.
На будь-якому етапі, що передує `handle{signal}`, ми можемо виконати сигнал вручну, викликавши метод [processSignal()|api:Nette\Application\UI\Presenter::processSignal()], який бере на себе обробку сигналу — бере компонент, який визначено як одержувача сигналу (якщо одержувач сигналу не вказаний, це сам presenter) і надсилає йому сигнал.
Приклад:
```php
if ($this->isSignalReceiver($this, 'paging') || $this->isSignalReceiver($this, 'sorting')) {
$this->processSignal();
}
```
Таким чином, сигнал виконано передчасно і більше не буде викликатися.