Komponenty interaktywne
Komponenty to osobne obiekty wielokrotnego użytku, które osadzamy w stronach. Mogą to być formularze, datagridy, ankiety, w zasadzie wszystko, co warto wykorzystywać wielokrotnie. Pokażemy:
- jak używać komponentów?
- jak je pisać?
- czym są sygnały?
Nette ma wbudowany system komponentów. Coś podobnego mogą kojarzyć weterani z Delphi albo ASP.NET Web Forms; React czy Vue.js zbudowane są na czymś odlegle podobnym. W świecie frameworków PHP jest to jednak funkcja unikalna.
Jednocześnie komponenty zasadniczo wpływają na podejście do tworzenia aplikacji. Możesz składać strony z wcześniej przygotowanych jednostek. Potrzebujesz datagrida w swojej administracji? Znajdź go na Componette, repozytorium dodatków open source (nie tylko komponentów) do Nette, i po prostu wstaw do presentera.
Do presentera możesz włączyć dowolną liczbę komponentów. A w niektórych komponentach możesz osadzić kolejne. Powstaje w ten sposób drzewo komponentów, którego korzeniem jest presenter.
Metody fabryczne
Jak komponenty trafiają do presentera i jak się ich potem używa? Zwykle za pomocą metod fabrycznych.
Fabryka komponentów to elegancki sposób tworzenia komponentów dopiero wtedy, gdy są rzeczywiście potrzebne (lazy / on
demand). Cała magia polega na zaimplementowaniu metody o nazwie createComponent<Nazwa>(), gdzie
<Nazwa> to nazwa tworzonego komponentu, która komponent tworzy i zwraca.
class DefaultPresenter extends Nette\Application\UI\Presenter
{
protected function createComponentPoll(): PollControl
{
$poll = new PollControl;
$poll->items = $this->items;
return $poll;
}
}
Ponieważ wszystkie komponenty tworzone są w osobnych metodach, kod staje się przejrzystszy.
Nazwy komponentów zawsze zaczynają się małą literą, mimo że w nazwie metody pisane są wielką.
Fabryk nigdy nie wywołujemy bezpośrednio; wywoływane są automatycznie przy pierwszym użyciu komponentu. Dzięki temu komponent tworzony jest we właściwym momencie i tylko wtedy, gdy naprawdę jest potrzebny. Jeśli komponentu nie użyjemy (np. przy żądaniu AJAX, gdzie przesyłana jest tylko część strony, albo przy cachowaniu szablonu), w ogóle nie zostanie utworzony, co oszczędza wydajność serwera.
// sięgamy po komponent i jeśli był to pierwszy raz,
// wywoła się createComponentPoll(), które go utworzy
$poll = $this->getComponent('poll');
// alternatywny zapis: $poll = $this['poll'];
W szablonie komponent można wyrenderować tagiem {control}. Nie ma więc potrzeby ręcznego przekazywania komponentów do szablonu.
<h2>Zagłosuj</h2>
{control poll}
Do dynamicznego tworzenia zmiennej liczby komponentów użyj Multipliera.
Metody fabryczne createComponent<Nazwa>() działają nie tylko w presenterach. W ten sam sposób możesz
zagnieździć komponent w innym komponencie, składając je w drzewo – przydaje się to na przykład przy osobno renderowanym
formularzu wewnątrz komponentu.
Styl hollywoodzki
Komponenty powszechnie korzystają ze świeżej techniki, którą lubimy nazywać stylem hollywoodzkim. Na pewno znasz frazes, który często słyszą uczestnicy castingów filmowych: “Nie dzwoń do nas, my zadzwonimy do ciebie”. I dokładnie o to chodzi.
W Nette, zamiast nieustannie zadawać pytania (“czy formularz został wysłany?”, “czy był poprawny?”, “czy użytkownik nacisnął ten przycisk?”), mówisz frameworkowi “gdy to się stanie, wywołaj tę metodę” i zostawiasz mu dalszą pracę. Jeśli programujesz w JavaScripcie, dobrze znasz ten styl programowania. Piszesz funkcje, które wywoływane są, gdy nastąpi określone zdarzenie. A język przekazuje im odpowiednie parametry.
Całkowicie zmienia to perspektywę pisania aplikacji. Im więcej zadań możesz zostawić frameworkowi, tym mniej masz pracy. I tym mniej możesz przeoczyć.
Pisanie komponentu
Przez pojęcie komponent rozumiemy zwykle potomka klasy Nette\Application\UI\Control. (Trafniej
byłoby używać terminu “controls”, ale w niektórych językach ma on inne znaczenie, a “komponenty” bardziej się
przyjęły.) Sam presenter Nette\Application\UI\Presenter również
jest potomkiem klasy Control.
use Nette\Application\UI\Control;
class PollControl extends Control
{
}
Renderowanie
Wiemy już, że do wyrenderowania komponentu służy tag {control nazwaKomponentu}. Wywołuje on w rzeczywistości
metodę render() komponentu, w której zajmujemy się renderowaniem. Mamy do dyspozycji, tak jak w presenterze, szablon Latte w zmiennej $this->template, do której
przekazujemy parametry. W odróżnieniu od presentera musimy podać plik szablonu i kazać go wyrenderować:
public function render(): void
{
// wstawiamy do szablonu jakieś parametry
$this->template->param = $value;
// i renderujemy go
$this->template->render(__DIR__ . '/poll.latte');
}
Tag {control} pozwala przekazać metodzie render() parametry:
{control poll $id, $message}
public function render(int $id, string $message): void
{
// ...
}
Czasem komponent może składać się z kilku części, które chcemy renderować osobno. Dla każdej z nich tworzymy
własną metodę renderującą, tutaj w przykładzie renderPaginator():
public function renderPaginator(): void
{
// ...
}
A w szablonie wywołujemy ją potem tak:
{control poll:paginator}
Dla lepszego zrozumienia warto wiedzieć, jak ten tag przekłada się na kod PHP.
{control poll}
{control poll:paginator 123, 'hello'}
przekłada się na:
$control->getComponent('poll')->render();
$control->getComponent('poll')->renderPaginator(123, 'hello');
Metoda getComponent() zwraca komponent poll, a na tym komponencie wywoływana jest metoda
render() albo renderPaginator(), jeśli w tagu po dwukropku podano inną metodę renderującą.
Uwaga, jeśli w parametrach poza nawiasami kwadratowymi pojawi się =>, wszystkie
parametry zostaną opakowane w tablicę i przekazane jako pierwszy argument:
{control poll, id: 123, message: 'hello'}
przekłada się na:
$control->getComponent('poll')->render(['id' => 123, 'message' => 'hello']);
Renderowanie subkomponentu:
{control cartControl-someForm}
przekłada się na:
$control->getComponent("cartControl-someForm")->render();
Komponenty, podobnie jak presentery, automatycznie przekazują do szablonów kilka przydatnych zmiennych:
$basePathto bezwzględna ścieżka URL do katalogu głównego (np./eshop)$baseUrlto bezwzględny URL katalogu głównego (np.http://localhost/eshop)$userto obiekt reprezentujący użytkownika$presenterto bieżący presenter$controlto bieżący komponent$flashesto tablica wiadomości wysłanych funkcjąflashMessage()
Sygnał
Wiemy już, że nawigacja w aplikacji Nette polega na linkowaniu albo przekierowywaniu do par Presenter:akcja.
A co, jeśli chcemy tylko wykonać akcję na bieżącej stronie? Na przykład zmienić sortowanie kolumn w tabeli;
usunąć element; przełączyć tryb jasny/ciemny; wysłać formularz; zagłosować w ankiecie itd.
Ten typ żądania nazywa się sygnałem. I tak jak akcje wywołują metody action<Akcja>() albo
render<Akcja>(), sygnały wywołują metody handle<Sygnał>(). Podczas gdy pojęcie akcji
(albo widoku) dotyczy wyłącznie presenterów, sygnały dotyczą wszystkich komponentów. A więc i presenterów, bo
UI\Presenter jest potomkiem UI\Control.
public function handleClick(int $x, int $y): void
{
// ... przetwarzanie sygnału ...
}
Odnośnik wywołujący sygnał tworzy się w zwykły sposób, czyli w szablonie atrybutem n:href albo tagiem
{link}, a w kodzie metodą link(). Więcej w rozdziale Tworzenie odnośników URL.
<a n:href="click! $x, $y">kliknij tutaj</a>
Sygnał wywoływany jest zawsze na bieżącym presenterze i akcji; nie da się wywołać go na innym presenterze albo innej akcji.
Sygnał powoduje więc przeładowanie strony dokładnie jak pierwotne żądanie, ale dodatkowo wywołuje metodę obsługującą sygnał z odpowiednimi parametrami. Jeśli metoda nie istnieje, zgłaszany jest wyjątek Nette\Application\UI\BadSignalException, który wyświetlany jest użytkownikowi jako strona błędu 403 Forbidden.
Snippety i AJAX
Sygnały mogą trochę przypominać Ci AJAX: handlery wywoływane na bieżącej stronie. I masz rację, sygnały rzeczywiście często wywoływane są przez AJAX, a następnie do przeglądarki przesyłane są tylko zmienione części strony. Nazywamy je snippetami. Więcej informacji znajdziesz na stronie poświęconej AJAX-owi.
Wiadomości flash
Komponent ma własny magazyn wiadomości flash, niezależny od presentera. To wiadomości informujące na przykład o wyniku operacji. Ważną cechą wiadomości flash jest to, że są dostępne w szablonie również po przekierowaniu. Nawet po wyświetleniu pozostają aktywne przez kolejne 30 sekund, na przykład na wypadek, gdyby użytkownik odświeżył stronę z powodu błędu transmisji – wiadomość nie zniknie od razu.
Wysyłaniem zajmuje się metoda flashMessage. Pierwszym
parametrem jest tekst wiadomości (string, Stringable) albo obiekt stdClass reprezentujący
wiadomość. Opcjonalnym drugim parametrem jest jej typ (error, warning, info itd.). Metoda flashMessage() zwraca
instancję wiadomości flash jako obiekt stdClass, do którego można dodać dalsze informacje.
$this->flashMessage('Element został usunięty.');
$this->redirect(/* ... */); // i przekierowujemy
Wiadomości te dostępne są w szablonie w zmiennej $flashes jako obiekty stdClass, które zawierają
właściwości message (tekst wiadomości), type (typ wiadomości) i mogą zawierać wspomniane
informacje użytkownika. Renderujemy je na przykład tak:
{foreach $flashes as $flash}
<div class="flash {$flash->type}">{$flash->message}</div>
{/foreach}
Przekierowanie po przetworzeniu sygnału
Po przetworzeniu sygnału komponentu często następuje przekierowanie. Przypomina to formularze – po ich wysłaniu również przekierowujemy, aby zapobiec ponownemu wysłaniu danych przy odświeżeniu strony w przeglądarce.
$this->redirect('this'); // przekierowuje na bieżący presenter i akcję
Ponieważ komponent jest elementem wielokrotnego użytku i zwykle 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 albo inną akcję, możesz zrobić to przez presenter:
$this->getPresenter()->redirect('Product:show'); // przekierowuje na inny presenter/akcję
Parametry trwałe
Parametry trwałe służą do utrzymywania stanu w komponentach między różnymi żądaniami. Ich wartość pozostaje taka sama również po kliknięciu w odnośnik. W odróżnieniu od danych w sesji przesyłane są w URL. I dzieje się to całkowicie automatycznie, łącznie z odnośnikami tworzonymi w innych komponentach na tej samej stronie.
Masz na przykład komponent do stronicowania treści. Takich komponentów może być na stronie kilka. I chcemy, aby po
kliknięciu w odnośnik wszystkie komponenty pozostały na swojej bieżącej stronie. Dlatego numer strony (page)
czynimy parametrem trwałym.
Utworzenie parametru trwałego w Nette jest wyjątkowo proste. Wystarczy utworzyć właściwość publiczną i oznaczyć ją
atrybutem: (wcześniej używano /** @persistent */)
use Nette\Application\Attributes\Persistent; // ten wiersz jest ważny
class PaginatingControl extends Control
{
#[Persistent]
public int $page = 1; // musi być public
}
Zalecamy podanie typu danych właściwości (np. int), możesz też podać wartość domyślną. Wartości
parametrów można walidować.
Przy tworzeniu odnośnika wartość parametru trwałego można zmienić:
<a n:href="this page: $page + 1">dalej</a>
Albo zresetować, czyli usunąć z URL. Przyjmie wtedy swoją wartość domyślną:
<a n:href="this page: null">resetuj</a>
Komponenty trwałe
Trwałe mogą być nie tylko parametry, ale też komponenty. Ich parametry trwałe przenoszone są wtedy również między
różnymi akcjami presentera albo między wieloma presenterami. Komponenty trwałe oznaczamy atrybutem na klasie presentera. Na
przykład komponenty calendar i poll oznaczymy tak:
use Nette\Application\Attributes\Persistent;
#[Persistent('calendar', 'poll')]
class DefaultPresenter extends Nette\Application\UI\Presenter
{
}
Subkomponentów wewnątrz tych komponentów nie trzeba oznaczać; również stają się trwałe.
Starsza adnotacja @persistent nadal działa, ale jest przestarzała i wywołuje ostrzeżenie:
/**
* @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” presenterów, które będą ich używać? Dzięki sprytnym możliwościom kontenera DI w Nette, podobnie jak przy klasycznych usługach, większość pracy można zostawić frameworkowi.
Weźmy przykład komponentu, 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 o czym mówić. Kontener DI niewidocznie zająłby się przekazaniem
wszystkich zależności. Przy komponentach zwykle jednak radzimy sobie tak, że tworzymy nową instancję bezpośrednio w
presenterze, w metodach fabrycznych createComponent…(). Ale przekazywanie
wszystkich zależności wszystkich komponentów do presentera tylko po to, aby przekazać je dalej do komponentów, jest
uciążliwe. A ile kodu do napisania…
Logiczne pytanie brzmi: dlaczego po prostu nie zarejestrujemy komponentu jako klasycznej usługi, nie przekażemy go do
presentera i nie zwrócimy w metodzie createComponent…()? To podejście jest jednak niewłaściwe, bo chcemy mieć
możliwość tworzenia komponentu w razie potrzeby wielokrotnie.
Poprawnym rozwiązaniem jest napisanie fabryki komponentu, czyli klasy, która tworzy komponent za nas:
class PollControlFactory
{
public function __construct(
private PollFacade $facade,
) {
}
public function create(int $id): PollControl
{
return new PollControl($id, $this->facade);
}
}
Fabrykę tę rejestrujemy w kontenerze w konfiguracji:
services:
- PollControlFactory
i wreszcie używamy 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ć własny parametr
return $this->pollControlFactory->create($pollId);
}
}
Świetne jest to, że Nette DI potrafi takie proste fabryki wygenerować, więc zamiast pisać cały jej kod, wystarczy napisać jej interfejs:
interface PollControlFactory
{
public function create(int $id): PollControl;
}
I to wszystko. Nette wewnętrznie implementuje ten interfejs i wstrzykuje go do presentera, gdzie możemy go użyć.
Magicznie dodaje do naszego komponentu parametr $id i instancję klasy PollFacade.
Komponenty w głąb
Komponenty w Nette Application reprezentują części aplikacji webowej wielokrotnego użytku, które osadzamy w stronach i którym poświęcony jest cały ten rozdział. Jakie dokładnie są możliwości takiego komponentu?
- da się go wyrenderować w szablonie
- wie, którą swoją część wyrenderować przy żądaniu AJAX (snippety)
- ma możliwość przechowywania swojego stanu w URL (parametry trwałe)
- ma możliwość reagowania na działania użytkownika (sygnały)
- tworzy strukturę hierarchiczną (której korzeniem jest presenter)
Za każdą z tych funkcji odpowiada jedna z klas w linii dziedziczenia. Za renderowanie (1 + 2) odpowiada Nette\Application\UI\Control, za włączenie w cykl życia (3, 4) klasa Nette\Application\UI\Component, a za utworzenie 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 trwałych
Wartości parametrów trwałych otrzymane z URL zapisywane są do właściwości metodą
loadState(). Sprawdza ona również, czy typ danych podany dla właściwości się zgadza; w przeciwnym razie
odpowiada błędem 404 i strona nie zostaje wyświetlona.
Nigdy nie ufaj ślepo parametrom trwałym, bo użytkownik może je łatwo nadpisać w URL. Tak sprawdzimy na przykład, czy
numer strony $this->page jest większy od 0. Odpowiednim sposobem jest nadpisanie wspomnianej metody
loadState():
class PaginatingControl extends Control
{
#[Persistent]
public int $page = 1;
public function loadState(array $params): void
{
parent::loadState($params); // tutaj ustawiane jest $this->page
// następuje własne sprawdzenie wartości:
if ($this->page < 1) {
$this->error();
}
}
}
Odwrotnym procesem, czyli zebraniem wartości z właściwości trwałych, zajmuje się metoda saveState().
Podłączenie do presentera
W chwili, gdy komponent staje się częścią hierarchii presentera, wywoływane są jego callbacki zapisane w tablicy
$onAnchor. Od tego momentu komponent ma dostępny presenter, może bezpiecznie tworzyć odnośniki, odczytywać
parametry trwałe itd.
$control->onAnchor[] = function ($control): void {
// komponent ma teraz dostępny presenter
};
Sygnały w głąb
Sygnał powoduje przeładowanie strony dokładnie jak pierwotne żądanie (poza wywołaniem 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<Sygnał>. Dalsze przetwarzanie zależy od danego
obiektu. Obiekty dziedziczące po Component (czyli Control i Presenter) reagują próbą
wywołania metody handle<Sygnał> z odpowiednimi parametrami.
Innymi słowy: brana jest definicja funkcji handle<Sygnał> wraz ze wszystkimi parametrami, które przyszły
z żądaniem, a parametry z URL przypisywane są do argumentów po nazwie, po czym następuje próba wywołania metody. Na
przykład wartość parametru id z URL przekazywana jest jako argument $id, something
z URL jako $something itd. A jeśli metoda nie istnieje, metoda signalReceived zgłasza wyjątek.
Poza parametrami z URL sygnał odczytuje również parametry wysłane w ciele POST żądania. Przydaje się to, bo sygnały często wywoływane są przez JavaScript, w którym naturalne jest wysyłanie danych metodą POST. Jeśli jednak parametr o tej samej nazwie przyjdzie zarówno z URL, jak i z ciała POST, pierwszeństwo ma wartość z URL. Unikaj więc nadawania polu POST tej samej nazwy co parametrowi URL albo trasy, bo wartość z URL po cichu by je nadpisała. Parametry sygnału dzielą wspólną przestrzeń z parametrami akcji i parametrami trwałymi, zobacz Wspólna przestrzeń parametrów.
Sygnał może odebrać dowolny komponent, presenter albo obiekt implementujący interfejs SignalReceiver
i 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 powinien coś zrobić: ankieta ma policzyć głos użytkownika, blok z newsami ma
się rozwinąć i wyświetlić dwa razy więcej newsów, formularz został wysłany i ma przetworzyć dane itd.
URL sygnału tworzy się metodą Component::link(). Jako
parametr $destination przekazujemy string {sygnał}!, a jako $args tablicę argumentów,
które chcemy przekazać sygnałowi. Sygnał wywoływany jest zawsze na bieżącym presenterze i akcji z bieżącymi
parametrami; parametry sygnału są tylko dodawane. Ponadto dodawany jest parametr ?do, który określa
sygnał.
Jego format to albo {sygnał}, albo {odbiorcaSygnału}-{sygnał}. {odbiorcaSygnału} to
nazwa komponentu w presenterze. Dlatego w nazwie komponentu nie można użyć myślnika – służy on do oddzielenia nazwy
komponentu i sygnału, choć w ten sposób można zagnieżdżać wiele komponentów.
Metoda isSignalReceiver()
sprawdza, czy komponent (pierwszy argument) jest odbiorcą sygnału (drugi argument). Drugi argument można pominąć –
sprawdzane jest wtedy, czy komponent jest odbiorcą jakiegokolwiek sygnału. Jeśli drugi parametr ustawimy na true,
sprawdzane jest, czy odbiorcą jest podany komponent albo któryś z jego potomków.
Na dowolnym etapie poprzedzającym handle<Sygnał> możemy wykonać sygnał ręcznie, wywołując metodę processSignal(),
która zajmuje się obsługą sygnału – bierze komponent zidentyfikowany jako odbiorca sygnału (jeśli odbiorcy nie podano,
jest nim sam presenter) i wysyła mu sygnał.
Przykład:
if ($this->isSignalReceiver($this, 'paging') || $this->isSignalReceiver($this, 'sorting')) {
$this->processSignal();
}
Sygnał zostanie w ten sposób wykonany przedwcześnie i nie zostanie wywołany ponownie.