Komponenty interaktywne
Komponenty to samodzielne obiekty wielokrotnego użytku, które wstawiamy na strony. Mogą to być formularze, datagridy, ankiety, właściwie wszystko, co ma sens używać wielokrotnie. Pokażemy:
- jak używać komponentów?
- jak je pisać?
- czym są sygnały?
Nette ma wbudowany system komponentów. Coś podobnego mogą pamiętać weterani z Delphi lub ASP.NET Web Forms, na czymś zdalnie podobnym opiera się React czy Vue.js. Jednak w świecie frameworków PHP jest to unikalna sprawa.
Przy tym komponenty zasadniczo wpływają na podejście do tworzenia aplikacji. Możesz bowiem składać strony z gotowych jednostek. Potrzebujesz w administracji datagrid? Znajdziesz go na Componette, repozytorium open-source dodatków (czyli nie tylko komponentów) dla Nette i po prostu wstawisz do presentera.
Do presentera możesz włączyć dowolną liczbę komponentów. A do niektórych komponentów możesz wstawiać kolejne komponenty. Powstaje w ten sposób drzewo komponentów, którego korzeniem jest presenter.
Metody fabrykujące
Jak wstawiać komponenty do presentera i następnie ich używać? Zazwyczaj za pomocą metod fabrykujących.
Fabryka komponentów stanowi elegancki sposób na tworzenie komponentów dopiero w chwili, gdy są rzeczywiście potrzebne
(lazy / on demand). Cały urok polega na implementacji metody o nazwie createComponent<Name>()
, gdzie
<Name>
to nazwa tworzonego komponentu, która tworzy i zwraca komponent.
class DefaultPresenter extends Nette\Application\UI\Presenter
{
protected function createComponentPoll(): PollControl
{
$poll = new PollControl;
$poll->items = $this->item;
return $poll;
}
}
Dzięki temu, że wszystkie komponenty są tworzone w osobnych metodach, kod zyskuje na przejrzystości.
Nazwy komponentów zawsze zaczynają się małą literą, mimo że w nazwie metody pisane są z dużej.
Fabryk nigdy nie wywołujemy bezpośrednio, wywołują się same w chwili, gdy komponent użyjemy po raz pierwszy. Dzięki temu komponent jest tworzony we właściwym momencie i tylko wtedy, gdy jest rzeczywiście potrzebny. Jeśli komponentu nie użyjemy (np. przy żądaniu AJAX, gdy przesyłana jest tylko część strony, lub przy cachowaniu szablonu), nie zostanie on w ogóle utworzony i oszczędzimy wydajność serwera.
// uzyskujemy dostęp do komponentu i jeśli to było po raz pierwszy,
// wywołuje się createComponentPoll(), która go tworzy
$poll = $this->getComponent('poll');
// alternatywna składnia: $poll = $this['poll'];
W szablonie można wyrenderować komponent za pomocą znacznika {control}. Nie ma więc potrzeby ręcznego przekazywania komponentów do szablonu.
<h2>Głosuj</h2>
{control poll}
Styl Hollywood
Komponenty często używają jednej świeżej techniki, którą lubimy nazywać stylem Hollywood. Na pewno znasz skrzydlate zdanie, które tak często słyszą uczestnicy castingów filmowych: „Nie dzwońcie do nas, my zadzwonimy do was”. I właśnie o to chodzi.
W Nette bowiem, zamiast ciągle pytać („czy formularz został wysłany?”, „czy był poprawny?” lub „czy użytkownik nacisnął ten przycisk?”), mówisz frameworkowi „kiedy to się stanie, wywołaj tę metodę” i zostawiasz dalszą pracę jemu. Jeśli programujesz w JavaScript, ten styl programowania jest Ci dobrze znany. Piszesz funkcje, które są wywoływane, gdy nastąpi określone zdarzenie. A język przekazuje im odpowiednie parametry.
To całkowicie zmienia spojrzenie na pisanie aplikacji. Im więcej zadań możesz zostawić frameworkowi, tym mniej masz pracy. I tym mniej możesz czegoś np. pominąć.
Pisanie komponentu
Pod pojęciem komponent zazwyczaj rozumiemy potomka klasy Nette\Application\UI\Control. (Dokładniej
byłoby więc używać terminu „controls”, ale „kontrolki” mają w języku polskim zupełnie inne znaczenie i raczej
przyjęły się „komponenty”.) Sam presenter Nette\Application\UI\Presenter jest
zresztą również potomkiem klasy Control
.
use Nette\Application\UI\Control;
class PollControl extends Control
{
}
Renderowanie
Już wiemy, że do renderowania komponentu używa się znacznika {control componentName}
. Ten znacznik właściwie
wywołuje metodę render()
komponentu, w której dbamy o renderowanie. Do dyspozycji mamy, tak samo jak w
presenterze, szablon Latte w zmiennej
$this->template
, do której przekazujemy parametry. W przeciwieństwie do presentera musimy podać plik
z szablonem i zlecić jego wyrenderowanie:
public function render(): void
{
// wstawiamy do szablonu jakieś parametry
$this->template->param = $value;
// i renderujemy go
$this->template->render(__DIR__ . '/poll.latte');
}
Znacznik {control}
umożliwia przekazanie parametrów do metody render()
:
{control poll $id, $message}
public function render(int $id, string $message): void
{
// ...
}
Czasami komponent może składać się z kilku części, które chcemy renderować oddzielnie. Dla każdej z nich tworzymy
własną metodę renderującą, tutaj w przykładzie np. renderPaginator()
:
public function renderPaginator(): void
{
// ...
}
A w szablonie wywołujemy ją za pomocą:
{control poll:paginator}
Dla lepszego zrozumienia warto wiedzieć, jak ten znacznik jest tłumaczony na PHP.
{control poll}
{control poll:paginator 123, 'hello'}
zostanie przetłumaczone jako:
$control->getComponent('poll')->render();
$control->getComponent('poll')->renderPaginator(123, 'hello');
Metoda getComponent()
zwraca komponent poll
i na tym komponencie wywołuje metodę
render()
, lub renderPaginator()
, jeśli inny sposób renderowania jest podany w znaczniku za
dwukropkiem.
Uwaga, jeśli gdziekolwiek w parametrach pojawi się =>
, wszystkie parametry zostaną
zapakowane do tablicy i przekazane jako pierwszy argument:
{control poll, id: 123, message: 'hello'}
zostanie przetłumaczone jako:
$control->getComponent('poll')->render(['id' => 123, 'message' => 'hello']);
Renderowanie podkomponentu:
{control cartControl-someForm}
zostanie przetłumaczone jako:
$control->getComponent("cartControl-someForm")->render();
Komponenty, podobnie jak presentery, automatycznie przekazują do szablonów kilka użytecznych zmiennych:
$basePath
to absolutna ścieżka URL do katalogu głównego (np./eshop
)$baseUrl
to absolutny URL do katalogu głównego (np.http://localhost/eshop
)$user
to obiekt reprezentujący użytkownika$presenter
to aktualny presenter$control
to aktualny komponent$flashes
to tablica wiadomości wysłanych przez funkcjęflashMessage()
Sygnał
Już wiemy, że nawigacja w aplikacji Nette polega na linkowaniu lub przekierowywaniu do par Presenter:action
. Ale
co, jeśli chcemy tylko wykonać akcję na aktualnej stronie? Na przykład zmienić sortowanie kolumn w tabeli; usunąć
pozycję; przełączyć tryb jasny/ciemny; wysłać formularz; zagłosować w ankiecie; itp.
Ten rodzaj żądań nazywa się sygnałami. I podobnie jak akcje wywołują metody action<Action>()
lub
render<Action>()
, sygnały wywołują metody handle<Signal>()
. Podczas gdy pojęcie akcji
(lub widoku) jest związane wyłącznie z presenterami, sygnały dotyczą wszystkich komponentów. A więc także presenterów,
ponieważ UI\Presenter
jest potomkiem UI\Control
.
public function handleClick(int $x, int $y): void
{
// ... przetwarzanie sygnału ...
}
Link, który wywoła sygnał, tworzymy w zwykły sposób, czyli w szablonie za pomocą atrybutu n:href
lub
znacznika {link}
, w kodzie za pomocą metody link()
. Więcej w rozdziale Tworzenie linków URL.
<a n:href="click! $x, $y">kliknij tutaj</a>
Sygnał zawsze jest wywoływany na aktualnym presenterze i akcji, nie można go wywołać na innym presenterze lub innej akcji.
Sygnał powoduje więc ponowne załadowanie strony dokładnie tak samo, jak przy pierwotnym żądaniu, tylko dodatkowo wywołuje metodę obsługującą sygnał z odpowiednimi parametrami. Jeśli metoda nie istnieje, rzucany jest wyjątek Nette\Application\UI\BadSignalException, który użytkownikowi wyświetla się jako strona błędu 403 Forbidden.
Snippety i AJAX
Sygnały mogą trochę przypominać AJAX: handlery, które są wywoływane na aktualnej stronie. I masz rację, sygnały naprawdę często są wywoływane za pomocą AJAXu, a następnie przesyłamy do przeglądarki tylko zmienione części strony. Czyli tzw. snippety. Więcej informacji znajdziesz na stronie poświęconej AJAX.
Wiadomości flash
Komponent ma własny magazyn wiadomości flash, niezależny od presentera. Są to wiadomości, które np. informują o wyniku operacji. Ważną cechą wiadomości flash jest to, że są dostępne w szablonie nawet po przekierowaniu. Nawet po wyświetleniu pozostają aktywne przez kolejne 30 sekund – na przykład na wypadek, gdyby z powodu błędnego transferu użytkownik odświeżył stronę – wiadomość mu więc od razu nie zniknie.
Wysyłanie zapewnia metoda flashMessage. Pierwszym
parametrem jest tekst wiadomości lub obiekt stdClass
reprezentujący wiadomość. Opcjonalnym drugim parametrem jest
jej typ (error, warning, info itp.). Metoda flashMessage()
zwraca instancję wiadomości flash jako obiekt
stdClass
, do którego można dodawać kolejne informacje.
$this->flashMessage('Pozycja została usunięta.');
$this->redirect(/* ... */); // i przekierowujemy
W szablonie te wiadomości są dostępne w zmiennej $flashes
jako obiekty stdClass
, które zawierają
właściwości message
(tekst wiadomości), type
(typ wiadomości) i mogą zawierać już wspomniane
informacje użytkownika. Wyrenderujemy je na przykład tak:
{foreach $flashes as $flash}
<div class="flash {$flash->type}">{$flash->message}</div>
{/foreach}
Przekierowanie po sygnale
Po przetworzeniu sygnału komponentu często następuje przekierowanie. Jest to podobna sytuacja jak w przypadku formularzy – po ich wysłaniu również przekierowujemy, aby przy odświeżeniu strony w przeglądarce nie doszło do ponownego wysłania danych.
$this->redirect('this') // przekierowuje na aktualny presenter i akcję
Ponieważ komponent jest elementem wielokrotnego użytku i zazwyczaj nie powinien mieć bezpośredniego powiązania
z konkretnymi presenterami, metody redirect()
i link()
automatycznie interpretują parametr jako
sygnał komponentu:
$this->redirect('click') // przekierowuje na sygnał 'click' tego samego komponentu
Jeśli potrzebujesz przekierować na inny presenter lub akcję, możesz to zrobić za pośrednictwem presentera:
$this->getPresenter()->redirect('Product:show'); // przekierowuje na inny presenter/akcję
Parametry persistentne
Parametry persistentne służą do utrzymywania stanu w komponentach między różnymi żądaniami. Ich wartość pozostaje taka sama nawet po kliknięciu na link. W przeciwieństwie do danych w sesji, są one przesyłane w URL. I to całkowicie automatycznie, w tym w linkach tworzonych w innych komponentach na tej samej stronie.
Masz np. komponent do paginacji treści. Takich komponentów może być na stronie kilka. I chcemy, aby po kliknięciu na link
wszystkie komponenty pozostały na swojej aktualnej stronie. Dlatego z numeru strony (page
) zrobimy parametr
persistentny.
Tworzenie parametru persistentnego w Nette jest niezwykle proste. Wystarczy utworzyć publiczną właściwość i oznaczyć
ją atrybutem: (wcześniej używano /** @persistent */
)
use Nette\Application\Attributes\Persistent; // ta linia jest ważna
class PaginatingControl extends Control
{
#[Persistent]
public int $page = 1; // musi być public
}
Przy właściwości zalecamy podanie również typu danych (np. int
) i można podać również wartość
domyślną. Wartości parametrów można walidować.
Podczas tworzenia linku można zmienić wartość parametru persistentnego:
<a n:href="this page: $page + 1">następna</a>
Lub można go zresetować, tj. usunąć z URL. Wtedy przyjmie swoją wartość domyślną:
<a n:href="this page: null">resetuj</a>
Komponenty persistentne
Nie tylko parametry, ale także komponenty mogą być persistentne. W przypadku takiego komponentu jego parametry persistentne
są przenoszone również między różnymi akcjami presentera lub między wieloma presenterami. Komponenty persistentne oznaczamy
adnotacją przy klasie presentera. Na przykład tak oznaczymy komponenty calendar
i poll
:
/**
* @persistent(calendar, poll)
*/
class DefaultPresenter extends Nette\Application\UI\Presenter
{
}
Podkomponentów wewnątrz tych komponentów nie trzeba oznaczać, staną się również persistentne.
W PHP 8 można również użyć atrybutów do oznaczenia komponentów persistentnych:
use Nette\Application\Attributes\Persistent;
#[Persistent('calendar', 'poll')]
class DefaultPresenter extends Nette\Application\UI\Presenter
{
}
Komponenty z zależnościami
Jak tworzyć komponenty z zależnościami, nie „zaśmiecając” sobie presenterów, które będą ich używać? Dzięki sprytnym właściwościom kontenera DI w Nette, podobnie jak przy używaniu klasycznych usług, można pozostawić większość pracy frameworkowi.
Weźmy jako przykład komponent, który ma zależność od usługi PollFacade
:
class PollControl extends Control
{
public function __construct(
private int $id, // Id ankiety dla której tworzymy komponent
private PollFacade $facade,
) {
}
public function handleVote(int $voteId): void
{
$this->facade->vote($this->id, $voteId);
// ...
}
}
Gdybyśmy pisali klasyczną usługę, nie byłoby problemu. O przekazanie wszystkich zależności zadbałby niewidocznie
kontener DI. Jednak z komponentami zazwyczaj postępujemy tak, że ich nową instancję tworzymy bezpośrednio w presenterze w metodach fabrykujących createComponent…()
. Ale przekazywanie wszystkich
zależności wszystkich komponentów do presentera, aby je następnie przekazać komponentom, jest uciążliwe. I tyle
napisanego kodu…
Logicznym pytaniem jest, dlaczego po prostu nie zarejestrujemy komponentu jako klasycznej usługi, nie przekażemy go do
presentera, a następnie w metodzie createComponent…()
nie zwrócimy? Takie podejście jest jednak nieodpowiednie,
ponieważ chcemy mieć możliwość tworzenia komponentu nawet wielokrotnie.
Prawidłowym rozwiązaniem jest napisanie dla komponentu fabryki, czyli klasy, która nam komponent utworzy:
class PollControlFactory
{
public function __construct(
private PollFacade $facade,
) {
}
public function create(int $id): PollControl
{
return new PollControl($id, $this->facade);
}
}
Taką fabrykę zarejestrujemy w naszym kontenerze w konfiguracji:
services:
- PollControlFactory
a na koniec użyjemy jej w naszym presenterze:
class PollPresenter extends Nette\Application\UI\Presenter
{
public function __construct(
private PollControlFactory $pollControlFactory,
) {
}
protected function createComponentPollControl(): PollControl
{
$pollId = 1; // możemy przekazać nasz parametr
return $this->pollControlFactory->create($pollId);
}
}
Świetne jest to, że Nette DI potrafi takie proste fabryki generować, więc zamiast całego jej kodu wystarczy napisać tylko jej interfejs:
interface PollControlFactory
{
public function create(int $id): PollControl;
}
I to wszystko. Nette wewnętrznie zaimplementuje ten interfejs i przekaże go do presentera, gdzie już możemy go używać.
Magicznie doda nam do naszego komponentu również parametr $id
i instancję klasy PollFacade
.
Komponenty dogłębnie
Komponenty w Nette Application stanowią części aplikacji internetowej wielokrotnego użytku, które wstawiamy na strony i którym poświęcony jest cały ten rozdział. Jakie dokładnie możliwości ma taki komponent?
- jest renderowalny w szablonie
- wie, którą swoją część ma wyrenderować przy żądaniu AJAX (snippety)
- ma możliwość zapisywania swojego stanu w URL (parametry persistentne)
- ma możliwość reagowania na akcje użytkownika (sygnały)
- tworzy strukturę hierarchiczną (gdzie korzeniem jest presenter)
Każdą z tych funkcji obsługuje któraś z klas linii dziedziczenia. Renderowanie (1 + 2) obsługuje Nette\Application\UI\Control, włączenie do cyklu życia (3, 4) klasa Nette\Application\UI\Component, a tworzenie struktury hierarchicznej (5) klasy Container i 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 }
Cykl życia komponentu
Walidacja parametrów persistentnych
Wartości parametrów persistentnych otrzymanych z URL zapisuje do właściwości
metoda loadState()
. Sprawdza ona również, czy odpowiada typ danych podany przy właściwości, w przeciwnym razie
odpowiada błędem 404 i strona się nie wyświetla.
Nigdy ślepo nie wierz parametrom persistentnym, ponieważ mogą być łatwo nadpisane przez użytkownika w URL. W ten sposób
na przykład sprawdzimy, czy numer strony $this->page
jest większy niż 0. Odpowiednią drogą jest nadpisanie
wspomnianej metody loadState()
:
class PaginatingControl extends Control
{
#[Persistent]
public int $page = 1;
public function loadState(array $params): void
{
parent::loadState($params); // tutaj ustawia się $this->page
// następuje własna kontrola wartości:
if ($this->page < 1) {
$this->error();
}
}
}
Proces odwrotny, czyli zebranie wartości z właściwości persistentnych, obsługuje metoda saveState()
.
Sygnały dogłębnie
Sygnał powoduje ponowne załadowanie strony dokładnie tak samo, jak przy pierwotnym żądaniu (z wyjątkiem przypadku, gdy
jest wywoływany przez AJAX) i wywołuje metodę signalReceived($signal)
, której domyślna implementacja w klasie
Nette\Application\UI\Component
próbuje wywołać metodę złożoną ze słów handle{signal}
. Dalsze
przetwarzanie zależy od danego obiektu. Obiekty dziedziczące po Component
(tj. Control
i
Presenter
) reagują tak, że próbują wywołać metodę handle{signal}
z odpowiednimi parametrami.
Innymi słowy: bierze się definicję funkcji handle{signal}
i wszystkie parametry, które przyszły
z żądaniem, a do argumentów według nazwy dopasowuje się parametry z URL i próbuje wywołać daną metodę. Np. jako
parametr $id
przekazuje się wartość z parametru id
w URL, jako $something
przekazuje
się something
z URL, itd. A jeśli metoda nie istnieje, metoda signalReceived
rzuca wyjątek.
Sygnał może odbierać dowolny komponent, presenter lub obiekt, który implementuje interfejs SignalReceiver
i jest podłączony do drzewa komponentów.
Głównymi odbiorcami sygnałów będą Presentery
i komponenty wizualne dziedziczące po Control
.
Sygnał ma służyć jako znak dla obiektu, że ma coś zrobić – ankieta ma zliczyć głos od użytkownika, blok
z nowościami ma się rozwinąć i wyświetlić dwa razy więcej nowości, formularz został wysłany i ma przetworzyć
dane itp.
URL dla sygnału tworzymy za pomocą metody Component::link(). Jako
parametr $destination
przekazujemy ciąg {signal}!
a jako $args
tablicę argumentów,
które chcemy przekazać sygnałowi. Sygnał zawsze jest wywoływany na aktualnym presenterze i akcji z aktualnymi parametrami,
parametry sygnału są tylko dodawane. Dodatkowo na początku dodawany jest parametr ?do
, który określa
sygnał.
Jego format to albo {signal}
, albo {signalReceiver}-{signal}
. {signalReceiver}
to nazwa
komponentu w presenterze. Dlatego w nazwie komponentu nie może być myślnika – używa się go do oddzielenia nazwy komponentu
i sygnału, jednak można w ten sposób zagnieździć kilka komponentów.
Metoda isSignalReceiver()
sprawdza, czy komponent (pierwszy argument) jest odbiorcą sygnału (drugi argument). Drugi argument możemy pominąć – wtedy
sprawdza, czy komponent jest odbiorcą jakiegokolwiek sygnału. Jako drugi parametr można podać true
i tym samym
sprawdzić, czy odbiorcą jest nie tylko podany komponent, ale także którykolwiek jego potomek.
W dowolnej fazie poprzedzającej handle{signal}
możemy wykonać sygnał ręcznie, wywołując metodę processSignal(),
która zajmuje się obsługą sygnału – bierze komponent, który został określony jako odbiorca sygnału (jeśli nie jest
określony odbiorca sygnału, jest to sam presenter) i wysyła mu sygnał.
Przykład:
if ($this->isSignalReceiver($this, 'paging') || $this->isSignalReceiver($this, 'sorting')) {
$this->processSignal();
}
Tym samym sygnał jest wykonany przedwcześnie i nie będzie już ponownie wywoływany.