Prezenterzy
Dowiedz się jak pisać prezentery i szablony w Nette. Po przeczytaniu będziesz wiedział:
- jak działa prezenter
- jakie są trwałe parametry
- jak rysować szablony
Wiemy już, że prezenter to klasa, która reprezentuje konkretną stronę aplikacji internetowej, np. stronę główną; produkt w sklepie internetowym; formularz logowania; sitemap feed itp. Aplikacja może mieć od jednego do tysięcy prezenterów. W innych frameworkach są one również nazywane kontrolerami.
Zazwyczaj termin prezenter odnosi się do potomka klasy Nette\Application\UI\Presenter, która jest przydatna do generowania interfejsów internetowych i którą omówimy w dalszej części tego rozdziału. W ogólnym sensie prezenterem jest dowolny obiekt, który implementuje interfejs Nette\Application\IPresenter.
Cykl życia prezentera
Zadaniem prezentera jest obsługa żądania i zwrócenie odpowiedzi (która może być stroną HTML, obrazem, przekierowaniem itp.)
W ten sposób na początku przekazywane jest do niego żądanie. Nie jest to bezpośrednio żądanie HTTP, lecz obiekt Nette\Application\Request, na który przy pomocy routera zostało przekształcone żądanie HTTP. Zwykle nie mamy styczności z tym obiektem, ponieważ prezenter sprytnie deleguje przetwarzanie żądania do innych metod, które teraz pokażemy.
Cykl życia prezentera
Rysunek przedstawia listę metod, które są wywoływane sekwencyjnie od góry do dołu, jeśli istnieją. Żaden z nich nie musi istnieć, możemy mieć całkowicie pusty prezenter bez ani jednej metody i zbudować na nim prostą statyczną stronę.
__construct()
Konstruktor tak naprawdę nie należy do cyklu życia prezentera, ponieważ jest wywoływany w momencie tworzenia obiektu. Ale wspominamy o tym ze względu na jego znaczenie. Konstruktor (wraz z metodą inject) służy do przekazywania zależności.
Prezenter nie powinien zaspokajać logiki biznesowej aplikacji, zapisywania do i odczytywania z bazy danych, wykonywania
obliczeń itp. Do tego właśnie służą klasy z warstwy, którą nazywamy modelem. Na przykład klasa
ArticleRepository
może być odpowiedzialna za wyszukiwanie i przechowywanie artykułów. Aby prezenter mógł
z nim pracować, będzie miał go przekazanego do
niego za pomocą zastrzyku zależności:
class ArticlePresenter extends Nette\Application\UI\Presenter
{
public function __construct(
private ArticleRepository $articles,
) {
}
}
startup()
Natychmiast po otrzymaniu żądania wywoływana jest metoda startup()
. Możesz go użyć do inicjalizacji
właściwości, sprawdzenia uprawnień użytkowników itp. Wymagane jest, aby metoda zawsze wywoływała przodka
parent::startup()
.
action<Action>(args...)
Podobna metoda render<View>()
. Podczas gdy render<View>()
Metoda ma na celu przygotowanie
danych dla konkretnego szablonu, który jest następnie renderowany. action<Action>()
obsługuje żądanie bez
podążania za renderowaniem szablonu. Na przykład, przetwarza dane, loguje użytkownika w lub z i tak dalej, a następnie przekierowuje w inne miejsce.
Ważne jest to, że action<Action>()
jest wywoływany przed render<View>()
więc możemy w
nim potencjalnie zmienić dalszy bieg historii, czyli zmienić szablon do wylosowania, a także metodę
render<View>()
który zostanie wywołany. Odbywa się to za pomocą strony setView('jineView')
.
Do metody przekazywane są parametry z żądania. Możliwe i zalecane jest podawanie typów do parametrów, np.
actionShow(int $id, ?string $slug = null)
– jeśli w parametrze id
zabraknie lub nie będzie on
liczbą całkowitą, prezenter zwróci błąd 404 i wyjdzie.
handle<Signal>(args...)
Metoda ta przetwarza tzw. sygnały, z którymi zapoznamy się w rozdziale poświęconym komponentom. Jest on przeznaczony głównie do komponentów i przetwarzania żądań AJAX.
Parametry z żądania są przekazywane do metody, tak jak w przypadku action<Action>()
, w tym
sprawdzanie typów.
beforeRender()
Metoda beforeRender
, jak sama nazwa wskazuje, jest wywoływana przed każdą metodą
render<View>()
. Jest używany do wspólnej konfiguracji szablonów, przekazywania zmiennych układu
i tak dalej.
render<View>(args...)
Miejsce, w którym przygotowujemy szablon do późniejszego renderowania, przekazujemy do niego dane itp.
Parametry z żądania są przekazywane do metody, tak jak w przypadku action<Action>()
, w tym
sprawdzanie typów.
public function renderShow(int $id): void
{
// pobieramy dane z modelu i przekazujemy je do szablonu
$this->template->article = $this->articles->getById($id);
}
afterRender()
Metoda afterRender
, jak sama nazwa wskazuje, jest wywoływana po każdej metodzie
render<View>()
. Jest on raczej rzadko używany.
shutdown()
Jest on wywoływany na końcu cyklu życia prezentera.
Dobra rada, zanim ruszymy dalej. Presenter jak widać może obsługiwać wiele akcji/widoków, więc może mieć wiele
metod render<View>()
. Zalecamy jednak projektowanie prezenterów z jedną lub jak najmniejszą
ilością akcji.
Wysyłanie odpowiedzi
Odpowiedź prezentera to zazwyczaj render szablonu ze stroną HTML, ale może to być również przesłanie pliku, JSON lub nawet przekierowanie do innej strony.
W dowolnym momencie cyklu życia możemy użyć dowolnej z poniższych metod, aby wysłać odpowiedź i jednocześnie wyjść z prezentera:
redirect()
,redirectPermanent()
,redirectUrl()
orazforward()
przekierowaniaerror()
kończy pracę prezentera z powodu błędusendJson($data)
prezenter kończy pracę i wysyła dane w formacie JSONsendTemplate()
prezenter kończy pracę i natychmiast renderuje szablonsendResponse($response)
prezenter wychodzi i wysyła swoją własną odpowiedźterminate()
prezenter wychodzi bez odpowiedzi
Jeśli nie wywołasz żadnej z tych metod, prezenter automatycznie przejdzie do renderowania szablonu. Dlaczego? Ponieważ w 99% przypadków chcemy renderować szablon, więc presenter przyjmuje to zachowanie jako domyślne i chce ułatwić nam pracę.
Tworzenie linków
Presenter ma metodę link()
, która może być użyta do tworzenia linków URL do innych prezenterów. Pierwszym
parametrem jest docelowy prezenter & akcja, a następnie przekazane argumenty, które mogą być określone jako tablica:
$url = $this->link('Product:show', $id);
$url = $this->link('Product:show', [$id, 'lang' => 'cs']);
W szablonie linki do innych prezenterów i wydarzeń tworzone są w następujący sposób:
<a n:href="Product:show $id">detail produktu</a>
Wystarczy wpisać znaną parę Presenter:action
zamiast prawdziwego adresu URL i podać dowolne parametry.
Sztuczka to n:href
, która mówi, że Latte obsłuży ten atrybut i wygeneruje prawdziwy adres URL. Więc w Nette
nie musisz w ogóle myśleć o adresach URL, tylko o prezenterach i wydarzeniach.
Więcej informacji na ten temat znajduje się w rozdziale Tworzenie linków URL.
Przekierowanie
Aby przekierować do innego prezentera, użyj metod redirect()
i forward()
, które mają bardzo
podobną składnię do metody link().
Metoda forward()
przechodzi natychmiast do nowego prezentera bez przekierowania HTTP:
$this->forward('Product:show');
Przykład tzw. tymczasowego przekierowania z kodem HTTP 302 (lub 303, jeśli aktualną metodą żądania jest POST):
$this->redirect('Product:show', $id);
Aby uzyskać trwałe przekierowanie z kodem HTTP 301, wykonaj następujące czynności:
$this->redirectPermanent('Product:show', $id);
Możesz przekierować na inny adres URL poza aplikacją, używając metody redirectUrl()
. Kod HTTP może być
określony jako drugi parametr, przy czym domyślnie jest to 302 (lub 303, jeśli bieżącą metodą żądania jest POST):
$this->redirectUrl('https://nette.org');
Przekierowanie natychmiast kończy prezentera, rzucając cichy wyjątek zakończenia
Nette\Application\AbortException
.
Przed przekierowaniem można wysłać wiadomości flash, czyli takie, które będą wyświetlane w szablonie po przekierowaniu.
Wiadomości błyskowe
Są to komunikaty informujące zazwyczaj o wyniku jakiejś operacji. Ważną cechą wiadomości flash jest to, że są one dostępne w szablonie nawet po przekierowaniu. Nawet po ich wyświetleniu pozostaną na żywo przez kolejne 30 sekund – na przykład w przypadku, gdy użytkownik odświeży stronę z powodu błędu transmisji – więc komunikat nie zniknie natychmiast.
Wystarczy wywołać metodę flashMessage(), a
prezenter zajmie się przekazaniem jej do szablonu. Pierwszy parametr to tekst komunikatu, a opcjonalny drugi to jego typ (błąd,
ostrzeżenie, info itp.). Metoda flashMessage()
zwraca instancję wiadomości flash, do której można dodać
dodatkowe informacje.
$this->flashMessage('Item has been deleted.');
$this->redirect(/* ... */); // i przekierować
Wiadomości te są dostępne dla szablonu w zmiennej $flashes
jako obiekty stdClass
, które
zawierają właściwości message
(tekst wiadomości), type
(typ wiadomości) oraz mogą zawierać
wspomniane już informacje o użytkowniku. Na przykład wyrenderujmy je w następujący sposób:
{foreach $flashes as $flash}
<div class="flash {$flash->type}">{$flash->message}</div>
{/foreach}
Error 404 i co.
Jeśli żądanie nie może zostać spełnione, na przykład dlatego, że artykuł, który chcemy wyświetlić, nie istnieje w
bazie danych, rzucamy błąd 404 za pomocą metody error(?string $message = null, int $httpCode = 404)
.
public function renderShow(int $id): void
{
$article = $this->articles->getById($id);
if (!$article) {
$this->error();
}
// ...
}
Kod błędu HTTP może być przekazany jako drugi parametr, domyślnie jest to 404. Metoda działa poprzez rzucenie wyjątku
Nette\Application\BadRequestException
, po którym Application
przekazuje kontrolę do error-presenter.
Który jest prezenterem, którego zadaniem jest wyświetlenie strony informującej o błędzie. Ustawienie prezentera błędów
odbywa się w konfiguracji aplikacji.
Wysyłanie JSON
Przykład akcji-metody, która wysyła dane w formacie JSON i wychodzi z prezentera:
public function actionData(): void
{
$data = ['hello' => 'nette'];
$this->sendJson($data);
}
Parametry żądania
Prezenter, jak również każdy komponent, uzyskuje swoje parametry z żądania HTTP. Ich wartości można pobrać za pomocą
metody getParameter($name)
lub getParameters()
. Wartości są ciągami lub tablicami ciągów,
zasadniczo surowymi danymi uzyskanymi bezpośrednio z adresu URL.
Dla większej wygody zalecamy udostępnianie parametrów za pośrednictwem właściwości. Wystarczy opatrzyć je adnotacją
#[Parameter]
atrybut:
use Nette\Application\Attributes\Parameter; // Ta linia jest ważna
class HomePresenter extends Nette\Application\UI\Presenter
{
#[Parameter]
public string $theme; // musi być publiczny
}
W przypadku właściwości sugerujemy określenie typu danych (np. string
). Nette automatycznie rzuci wartość na
jej podstawie. Wartości parametrów mogą być również walidowane.
Podczas tworzenia linku można bezpośrednio ustawić wartość parametrów:
<a n:href="Home:default theme: dark">click</a>
Trwałe parametry
Trwałe parametry są używane do utrzymania stanu pomiędzy różnymi żądaniami. Ich wartość pozostaje taka sama nawet po
kliknięciu linku. W przeciwieństwie do danych sesji, są one przekazywane w adresie URL. Dzieje się to całkowicie
automatycznie, więc nie ma potrzeby wyraźnego ich podawania link()
lub n:href
.
Przykład użycia? Masz wielojęzyczną aplikację. Rzeczywisty język jest parametrem, który musi być częścią adresu URL
przez cały czas. Ale byłoby to niewiarygodnie żmudne, aby zawrzeć go w każdym linku. Więc robisz z niego trwały parametr
o nazwie lang
i będzie się on sam przenosił. Fajnie!
Tworzenie trwałych parametrów jest niezwykle proste w Nette. Wystarczy stworzyć właściwość publiczną i oznaczyć ją
atrybutem: (poprzednio używano /** @persistent */
)
use Nette\Application\Attributes\Persistent; // ta linia jest ważna
class ProductPresenter extends Nette\Application\UI\Presenter
{
#[Persistent]
public string $lang; // musi być publiczny
}
Jeśli $this->lang
ma wartość taką jak 'en'
, to linki utworzone przy użyciu
link()
lub n:href
będą zawierały również parametr lang=en
. A gdy link zostanie
kliknięty, ponownie będzie to $this->lang = 'en'
.
W przypadku właściwości zalecamy podanie typu danych (np. string
), a także wartości domyślnej. Wartości
parametrów mogą być walidowane.
Trwałe parametry są domyślnie przekazywane pomiędzy wszystkimi akcjami danego prezentera. Aby przekazać je pomiędzy wieloma prezenterami, musisz je zdefiniować:
- we wspólnym przodku, po którym dziedziczą prezentery
- w cechach, które są używane przez prezenterów:
trait LanguageAware
{
#[Persistent]
public string $lang;
}
class ProductPresenter extends Nette\Application\UI\Presenter
{
use LanguageAware;
}
Możesz zmienić wartość trwałego parametru podczas tworzenia łącza:
<a n:href="Product:show $id, lang: cs">detail in Czech</a>
Można też go resetować, czyli usunąć z adresu URL. Przyjmie on wtedy swoją domyślną wartość:
<a n:href="Product:show $id, lang: null">click</a>
Komponenty interaktywne
Prezenterzy mają wbudowany system komponentów. Komponenty to samodzielne jednostki wielokrotnego użytku, które wstawiamy do prezenterów. Mogą to być formularze, datagridy, menu, w rzeczywistości wszystko, co ma sens, aby używać wielokrotnie.
Jak komponenty są wstawiane do prezentera, a następnie wykorzystywane? Dowiesz się tego w rozdziale Komponenty. Dowiesz się nawet, co łączy ich z Hollywood.
A gdzie mogę dostać komponenty? Na stronie Componette można znaleźć komponenty open-source, a także szereg innych dodatków dla Nette, umieszczonych tu przez wolontariuszy ze społeczności skupionej wokół frameworka.
Sięgając głębiej
Z tym, co do tej pory omówiliśmy w tym rozdziale, jesteś prawdopodobnie całkowicie zadowolony. Kolejne wiersze są dla tych, którzy interesują się prezenterami dogłębnie i chcą wiedzieć wszystko.
Walidacja parametrów
Wartości parametrów żądania i parametrów
stałych otrzymanych z adresów URL są zapisywane we właściwościach przez metodę loadState()
. Sprawdza
również, czy typ danych określony we właściwości jest zgodny, w przeciwnym razie odpowie błędem 404, a strona nie zostanie
wyświetlona.
Nigdy nie należy ślepo ufać parametrom, ponieważ mogą one zostać łatwo nadpisane przez użytkownika w adresie URL. Na
przykład w ten sposób sprawdzamy, czy $this->lang
należy do obsługiwanych języków. Dobrym sposobem na to
jest nadpisanie metody loadState()
wspomnianej powyżej:
class ProductPresenter extends Nette\Application\UI\Presenter
{
#[Persistent]
public string $lang;
public function loadState(array $params): void
{
parent::loadState($params); // tutaj jest ustawiony $this->lang
// następuje sprawdzenie wartości użytkownika:
if (!in_array($this->lang, ['en', 'cs'])) {
$this->error();
}
}
}
Zapisywanie i przywracanie wniosku
Żądanie obsługiwane przez prezentera jest obiektem Nette\Application\Request i jest zwracane
przez metodę prezentera getRequest()
.
Możesz zapisać bieżące żądanie w sesji lub przywrócić je z sesji i pozwolić prezenterowi wykonać je ponownie. Jest
to przydatne na przykład, gdy użytkownik wypełnia formularz, a jego login wygasa. Aby nie utracić danych, przed
przekierowaniem na stronę logowania zapisujemy bieżące żądanie do sesji za pomocą
$reqId = $this->storeRequest()
, która zwraca identyfikator w postaci krótkiego ciągu znaków i przekazuje go
jako parametr do prezentera logowania.
Po zalogowaniu się wywołujemy metodę $this->restoreRequest($reqId)
, która pobiera żądanie z sesji
i przekazuje je dalej. Ta metoda sprawdza, czy żądanie zostało utworzone przez tego samego użytkownika, który jest teraz
zalogowany. Jeśli zalogował się inny użytkownik lub klucz był nieważny, nie robi nic i program kontynuuje.
Zobacz poradnik Jak powrócić do wcześniejszej strony.
Canonicalization
Prezentery mają jedną naprawdę fajną funkcję, która przyczynia się do lepszego SEO (optymalizacji możliwości
znalezienia użytkownika w internecie). Automatycznie zapobiegają one istnieniu zduplikowanej treści na różnych adresach URL.
Jeśli wiele adresów URL prowadzi do określonego miejsca docelowego, np. /index
i /index?page=1
,
framework określa jeden z nich jako główny (kanoniczny) i przekierowuje do niego pozostałe za pomocą kodu HTTP
301. Dzięki temu wyszukiwarki nie będą podwójnie indeksować Twojej witryny i rozcieńczać jej page rank.
Proces ten nazywany jest kanonikalizacją. Kanoniczny adres URL to ten wygenerowany przez router, zwykle pierwszy pasujący router w kolekcji.
Kanonizacja jest domyślnie włączona i można ją wyłączyć poprzez $this->autoCanonicalize = false
.
Przekierowanie nie nastąpi w przypadku żądań AJAX lub POST, ponieważ spowodowałoby to utratę danych lub nie przyniosłoby wartości dodanej z perspektywy SEO.
Możesz również wywołać kanonizację ręcznie za pomocą metody canonicalize()
, która przekaże prezentera,
akcję i parametry podobnie jak w przypadku metody link()
. Tworzy link i porównuje go z bieżącym adresem URL.
Jeśli się różni, przekierowuje na wygenerowany link.
public function actionShow(int $id, ?string $slug = null): void
{
$realSlug = $this->facade->getSlugForId($id);
// přesměruje, pokud $slug se liší od $realSlug
$this->canonicalize('Product:show', [$id, $realSlug]);
}
Wydarzenia
Oprócz metod startup()
, beforeRender()
i shutdown()
, które są wywoływane w ramach
cyklu życia prezentera, można zdefiniować dodatkowe funkcje, które będą wywoływane automatycznie. Presenter definiuje tzw.
zdarzenia, których handlery dodajesz do pól $onStartup
,
$onRender
oraz $onShutdown
.
class ArticlePresenter extends Nette\Application\UI\Presenter
{
public function __construct()
{
$this->onStartup[] = function () {
// ...
};
}
}
Handlery w polu $onStartup
są wywoływane tuż przed metodą startup()
, następnie
$onRender
pomiędzy beforeRender()
i render<View>()
i wreszcie
$onShutdown
tuż przed shutdown()
.
Odpowiedzi
Odpowiedź zwracana przez prezentera jest obiektem implementującym interfejs Nette\Application\Response. Dostępnych jest wiele gotowych odpowiedzi:
- Nette\Application\Responses\CallbackResponse – wysyła wywołanie zwrotne
- Nette\Application\Responses\FileResponse – wysyła plik
- Nette\Application\Responses\ForwardResponse – forward()
- Nette\Application\Responses\JsonResponse – wysyła JSON
- Nette\Application\Responses\RedirectResponse – przekierowanie
- Nette\Application\Responses\TextResponse – wyślij tekst
- Nette\Application\Responses\VoidResponse – pusta odpowiedź
Odpowiedzi wysyłane są za pomocą metody sendResponse()
:
use Nette\Application\Responses;
// Zwykły tekst
$this->sendResponse(new Responses\TextResponse('Hello Nette!'));
// Wysyła plik
$this->sendResponse(new Responses\FileResponse(__DIR__ . '/invoice.pdf', 'Invoice13.pdf'));
// Wysyła wywołanie zwrotne
$callback = function (Nette\Http\IRequest $httpRequest, Nette\Http\IResponse $httpResponse) {
if ($httpResponse->getHeader('Content-Type') === 'text/html') {
echo '<h1>Hello</h1>';
}
};
$this->sendResponse(new Responses\CallbackResponse($callback));
Ograniczenie dostępu przy użyciu
#[Requires]
Atrybut #[Requires]
zapewnia zaawansowane opcje ograniczania dostępu do prezenterów i ich metod. Można go
użyć do określenia metod HTTP, wymagania żądań AJAX, ograniczenia dostępu do tego samego źródła i ograniczenia dostępu
tylko do przekazywania. Atrybut może być stosowany do klas prezenterów, jak również poszczególnych metod, takich jak
action<Action>()
, render<View>()
, handle<Signal>()
, i
createComponent<Name>()
.
Można określić te ograniczenia:
- na metodach HTTP:
#[Requires(methods: ['GET', 'POST'])]
- wymagające żądania AJAX:
#[Requires(ajax: true)]
- dostęp tylko z tego samego źródła:
#[Requires(sameOrigin: true)]
- dostęp tylko przez przekierowanie:
#[Requires(forward: true)]
- ograniczenia dotyczące określonych działań:
#[Requires(actions: 'default')]
Aby uzyskać szczegółowe informacje, zobacz Jak używać atrybutu Requires atrybut.
Sprawdzanie metod HTTP
W Nette prezenterzy automatycznie weryfikują metodę HTTP każdego przychodzącego żądania głównie ze względów
bezpieczeństwa. Domyślnie dozwolone są metody GET
, POST
, HEAD
, PUT
,
DELETE
, PATCH
.
Jeśli chcesz włączyć dodatkowe metody, takie jak OPTIONS
, możesz użyć atrybutu #[Requires]
(od
wersji Nette Application v3.2):
#[Requires(methods: ['GET', 'POST', 'HEAD', 'PUT', 'DELETE', 'PATCH', 'OPTIONS'])]
class MyPresenter extends Nette\Application\UI\Presenter
{
}
W wersji 3.1 weryfikacja jest wykonywana w checkHttpMethod()
, która sprawdza, czy metoda określona w żądaniu
znajduje się w tablicy $presenter->allowedMethods
. Należy dodać taką metodę:
class MyPresenter extends Nette\Application\UI\Presenter
{
protected function checkHttpMethod(): void
{
$this->allowedMethods[] = 'OPTIONS';
parent::checkHttpMethod();
}
}
Ważne jest, aby podkreślić, że jeśli włączysz metodę OPTIONS
, musisz również poprawnie obsługiwać ją
w swoim prezenterze. Metoda ta jest często używana jako tak zwane żądanie wstępne, które przeglądarki automatycznie
wysyłają przed faktycznym żądaniem, gdy konieczne jest ustalenie, czy żądanie jest dozwolone z punktu widzenia polityki
CORS (Cross-Origin Resource Sharing). Jeśli zezwolisz na tę metodę, ale nie zaimplementujesz odpowiedniej odpowiedzi, może to
prowadzić do niespójności i potencjalnych problemów z bezpieczeństwem.