Интерактивни компоненти
Компонентите са самостоятелни обекти за многократна употреба, които вмъкваме в страниците. Това могат да бъдат формуляри, datagrid-ове, анкети, всъщност всичко, което има смисъл да се използва многократно. Ще покажем:
- как да използваме компоненти?
- как да ги пишем?
- какво са сигналите?
Nette има вградена компонентна система. Нещо подобно може да е познато на ветераните от Delphi или ASP.NET Web Forms, на нещо отдалечено подобно са базирани React или Vue.js. Въпреки това, в света на PHP фреймуърците това е уникално явление.
При това компонентите фундаментално влияят на подхода към създаването на приложения. Можете да сглобявате страници от предварително подготвени единици. Нуждаете се от datagrid в администрацията? Намерете го на Componette, хранилище на open-source добавки (т.е. не само компоненти) за Nette и просто го вмъкнете в презентера.
В презентера можете да включите произволен брой компоненти. А в някои компоненти можете да вмъквате други компоненти. Така се създава компонентно дърво, чийто корен е презентерът.
Фабрични методи
Как се вмъкват компоненти в презентера и след това се използват? Обикновено с помощта на фабрични методи.
Фабриката за компоненти представлява елегантен начин за създаване
на компоненти едва в момента, когато те са наистина необходими (lazy / on
demand). Цялата магия се състои в имплементирането на метод с име
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}
Hollywood style
Компонентите обикновено използват една свежа техника, която обичаме да наричаме Hollywood style. Със сигурност познавате крилатата фраза, която толкова често чуват участниците във филмови кастинги: „Не ни звънете, ние ще ви се обадим“. И точно за това става въпрос.
В Nette, вместо постоянно да се налага да питате нещо („беше ли изпратен формулярът?“, „беше ли валиден?“ или „натисна ли потребителят този бутон?“), казвате на фреймуърка „когато това се случи, извикай този метод“ и оставяте по-нататъшната работа на него. Ако програмирате на JavaScript, този стил на програмиране ви е добре познат. Пишете функции, които се извикват, когато настъпи определено събитие. И езикът им предава съответните параметри.
Това напълно променя гледната точка към писането на приложения. Колкото повече задачи можете да оставите на фреймуърка, толкова по-малко работа имате вие. И толкова по-малко неща можете да пропуснете.
Пишем компонент
Под понятието компонент обикновено разбираме наследник на клас Nette\Application\UI\Control.
(По-точно би било да се използва терминът „controls“, но „контроли“ на
български има съвсем различно значение и по-скоро се е наложило
„компоненти“.) Самият презентер 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 път до коренната директория (напр./eshop
)$baseUrl
е абсолютният URL до коренната директория (напр.http://localhost/eshop
)$user
е обект представляващ потребителя$presenter
е текущият презентер$control
е текущият компонент$flashes
масив от съобщения, изпратени с функциятаflashMessage()
Сигнал
Вече знаем, че навигацията в Nette приложение се състои в свързване или
пренасочване към двойки Presenter:action
. Но какво, ако просто искаме да
извършим действие на текущата страница? Например да променим
сортирането на колони в таблица; да изтрием елемент; да превключим
светъл/тъмен режим; да изпратим формуляр; да гласуваме в анкета;
и т.н.
Този вид заявки се наричат сигнали. И подобно на действията, които
извикват методи action<Action>()
или render<Action>()
, сигналите
извикват методи handle<Signal>()
. Докато понятието действие (или view)
е свързано чисто само с презентерите, сигналите се отнасят до всички
компоненти. И следователно и до презентерите, защото 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.
Flash съобщения
Компонентът има собствено хранилище за flash съобщения, независимо от презентера. Това са съобщения, които например информират за резултата от операция. Важна характеристика на flash съобщенията е, че те са достъпни в шаблона и след пренасочване. Дори след показване остават активни още 30 секунди – например в случай, че поради грешка при прехвърлянето потребителят обнови страницата – съобщението няма да изчезне веднага.
Изпращането се извършва от метода flashMessage.
Първият параметър е текстът на съобщението или обект stdClass
,
представляващ съобщението. Незадължителният втори параметър е
неговият тип (error, warning, info и др.). Методът 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') // пренасочва към текущия презентер и действие
Тъй като компонентът е елемент за многократна употреба и обикновено
не трябва да има пряка връзка с конкретни презентери, методите
redirect()
и link()
автоматично интерпретират параметъра като
сигнал на компонента:
$this->redirect('click') // пренасочва към сигнала 'click' на същия компонент
Ако трябва да пренасочите към друг презентер или действие, можете да го направите чрез презентера:
$this->getPresenter()->redirect('Product:show'); // пренасочва към друг презентер/действие
Персистентни параметри
Персистентните параметри служат за поддържане на състоянието в компонентите между различни заявки. Тяхната стойност остава същата и след кликване върху връзка. За разлика от данните в сесията, те се пренасят в URL. И това става напълно автоматично, включително за връзки, създадени в други компоненти на същата страница.
Имате например компонент за пагиниране на съдържание. Такива
компоненти могат да бъдат няколко на страницата. И искаме след
кликване върху връзка всички компоненти да останат на своята текуща
страница. Затова от номера на страницата (page
) ще направим
персистентен параметър.
Създаването на персистентен параметър в Nette е изключително лесно.
Достатъчно е да създадете публично свойство и да го маркирате с
атрибут: (преди се използваше /** @persistent */
)
use Nette\Application\Attributes\Persistent; // този ред е важен
class PaginatingControl extends Control
{
#[Persistent]
public int $page = 1; // трябва да е public
}
При свойството препоръчваме да посочите и типа данни (напр. 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);
}
}
Така регистрираме фабриката в нашия контейнер в конфигурацията:
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}
и
всички параметри, които са дошли със заявката, и към аргументите се
присвояват параметри от URL по име и се опитва да се извика даденият
метод. Напр. като параметър $id
се предава стойността от
параметъра id
в URL, като $something
се предава something
от URL
и т.н. И ако методът не съществува, методът signalReceived
хвърля изключение.
Сигнал може да приема всякакъв компонент, презентер или обект, който
имплементира интерфейса SignalReceiver
и е свързан към дървото на
компонентите.
Сред основните получатели на сигнали ще бъдат Presenters
и
визуалните компоненти, наследяващи Control
. Сигналът трябва да
служи като знак за обекта, че трябва да направи нещо – анкетата трябва
да преброи гласа от потребителя, блокът с новини трябва да се разгъне и
да покаже два пъти повече новини, формулярът е изпратен и трябва да
обработи данните и т.н.
URL за сигнал създаваме с помощта на метода Component::link(). Като
параметър $destination
предаваме низ {signal}!
и като $args
масив от аргументи, които искаме да предадем на сигнала. Сигналът
винаги се извиква на текущия презентер и действие с текущите
параметри, параметрите на сигнала само се добавят. Освен това в
началото се добавя параметър ?do
, който определя сигнала.
Неговият формат е или {signal}
, или {signalReceiver}-{signal}
.
{signalReceiver}
е името на компонента в презентера. Затова в името на
компонента не може да има тире – използва се за разделяне на името на
компонента и сигнала, но е възможно така да се вложат няколко
компонента.
Методът isSignalReceiver()
проверява дали компонентът (първи аргумент) е получател на сигнала
(втори аргумент). Вторият аргумент можем да пропуснем – тогава се
проверява дали компонентът е получател на какъвто и да е сигнал. Като
втори параметър може да се посочи true
и така да се провери дали
получател е не само посоченият компонент, но и който и да е негов
наследник.
Във всяка фаза, предхождаща handle{signal}
, можем да изпълним сигнала
ръчно, като извикаме метода processSignal(),
който поема отговорността за обработката на сигнала – взема
компонента, който е определен като получател на сигнала (ако не е
определен получател на сигнала, това е самият презентер) и му изпраща
сигнала.
Пример:
if ($this->isSignalReceiver($this, 'paging') || $this->isSignalReceiver($this, 'sorting')) {
$this->processSignal();
}
Така сигналът е изпълнен преждевременно и вече няма да се извиква отново.