Komponenty interaktywne
***********************
<div class=perex>
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?
</div>
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 |https://componette.org/search/component], 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.
```php .{file:DefaultPresenter.php}
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.
.[note]
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.
```php .{file:DefaultPresenter.php}
// 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} |#Renderowanie]. Nie ma więc potrzeby ręcznego przekazywania komponentów do szablonu.
```latte
<h2>Zagłosuj</h2>
{control poll}
```
.[tip]
Do dynamicznego tworzenia zmiennej liczby komponentów użyj [Multipliera |multiplier].
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 [api: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 [api:Nette\Application\UI\Presenter] również jest potomkiem klasy `Control`.
```php .{file:PollControl.php}
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|templates] w zmiennej `$this->template`, do której przekazujemy parametry. W odróżnieniu od presentera musimy podać plik szablonu i kazać go wyrenderować:
```php .{file:PollControl.php}
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:
```latte
{control poll, $id, $message}
```
```php .{file:PollControl.php}
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()`:
```php .{file:PollControl.php}
public function renderPaginator(): void
{
// ...
}
```
A w szablonie wywołujemy ją potem tak:
```latte
{control poll:paginator}
```
Dla lepszego zrozumienia warto wiedzieć, jak ten tag przekłada się na kod PHP.
```latte
{control poll}
{control poll:paginator, 123, 'hello'}
```
przekłada się na:
```php
$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ą.
.[caution]
Uwaga, jeśli w parametrach poza nawiasami kwadratowymi pojawi się **`=>`**, wszystkie parametry zostaną opakowane w tablicę i przekazane jako pierwszy argument:
```latte
{control poll, id: 123, message: 'hello'}
```
przekłada się na:
```php
$control->getComponent('poll')->render(['id' => 123, 'message' => 'hello']);
```
Renderowanie subkomponentu:
```latte
{control cartControl-someForm}
```
przekłada się na:
```php
$control->getComponent("cartControl-someForm")->render();
```
Komponenty, podobnie jak presentery, automatycznie przekazują do szablonów kilka przydatnych zmiennych:
- `$basePath` to bezwzględna ścieżka URL do katalogu głównego (np. `/eshop`)
- `$baseUrl` to bezwzględny URL katalogu głównego (np. `http://localhost/eshop`)
- `$user` to obiekt [reprezentujący użytkownika |security:authentication]
- `$presenter` to bieżący presenter
- `$control` to bieżący komponent
- `$flashes` to tablica [wiadomości |#Wiadomości flash] 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`.
```php
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 |creating-links#Odnośniki do sygnału].
```latte
<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 [api: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 |ajax].
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 |api:Nette\Application\UI\Control::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.
```php
$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:
```latte
{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.
```php
$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:
```php
$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:
```php
$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 */`)
```php
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ć |#Walidacja parametrów trwałych].
Przy tworzeniu odnośnika wartość parametru trwałego można zmienić:
```latte
<a n:href="this, page: $page + 1">dalej</a>
```
Albo *zresetować*, czyli usunąć z URL. Przyjmie wtedy swoją wartość domyślną:
```latte
<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:
```php
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:
```php
/**
* @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`:
```php
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 |#Metody fabryczne] `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:
```php
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:
```neon
services:
- PollControlFactory
```
i wreszcie używamy jej w naszym presenterze:
```php
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ć |dependency-injection:factory], więc zamiast pisać cały jej kod, wystarczy napisać jej interfejs:
```php
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?
1) da się go wyrenderować w szablonie
2) wie, [którą swoją część |ajax#Snippety] wyrenderować przy żądaniu AJAX (snippety)
3) ma możliwość przechowywania swojego stanu w URL (parametry trwałe)
4) ma możliwość reagowania na działania użytkownika (sygnały)
5) 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 [api:Nette\Application\UI\Control], za włączenie w [cykl życia |presenters#Cykl życia presentera] (3, 4) klasa [api:Nette\Application\UI\Component], a za utworzenie struktury hierarchicznej (5) klasy [Container i 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 }
```
Cykl życia komponentu
---------------------
[* lifecycle-component.svg *] *** *Cykl życia komponentu* .<>
Walidacja parametrów trwałych
-----------------------------
Wartości [parametrów trwałych |#Parametry trwałe] 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()`:
```php
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.
```php
$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 |api:Nette\Application\UI\BadSignalException].
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 |presenters#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() |api:Nette\Application\UI\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()|api:Nette\Application\UI\Presenter::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()|api:Nette\Application\UI\Presenter::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:
```php
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.
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.
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:
$flashes to 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:
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 fabrycznychcreateComponent…(). 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?
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.