Інтерактивні компоненти
Компоненти – це окремі об'єкти багаторазового використання, які ми поміщаємо на сторінки. Це можуть бути форми, сітки даних, опитування, загалом, усе, що має сенс використовувати багаторазово. Далі ми дізнаємося:
- як використовувати компоненти?
- як їх писати?
- що таке сигнали?
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 – це багаторазово використовувані частини веб-додатка, які ми вбудовуємо в сторінки, про що і піде мова в цьому розділі. Які можливості такого компонента?
- він може бути відображений у шаблоні
- він знає, яку частину себе рендерити під час AJAX-запиту (фрагменти)
- має можливість зберігати свій стан в URL (постійні параметри)
- має можливість реагувати на дії користувача (сигнали)
- створює ієрархічну структуру (де коренем є ведучий)
Кожна з цих функцій обробляється одним із класів лінії успадкування. Рендеринг (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();
}
Сигнал виконується передчасно і більше не буде викликаний.