Presentery

Zapoznamy się z tym, jak w Nette pisze się presentery i szablony. Po przeczytaniu będziesz wiedzieć:

  • jak działa presenter
  • co to są parametry trwałe
  • jak rysuje się szablony

Już wiemy, że presenter to klasa, która reprezentuje jakąś konkretną stronę aplikacji internetowej, np. stronę główną; produkt w e-sklepie; formularz logowania; kanał sitemap itp. Aplikacja może mieć od jednego do tysięcy presenterów. W innych frameworkach nazywa się je również kontrolerami.

Zazwyczaj pod pojęciem presenter rozumie się potomka klasy Nette\Application\UI\Presenter, który jest odpowiedni do generowania interfejsów internetowych i któremu poświęcimy resztę tego rozdziału. W ogólnym sensie presenter to dowolny obiekt implementujący interfejs Nette\Application\IPresenter.

Cykl życia presentera

Zadaniem presentera jest obsłużenie żądania i zwrócenie odpowiedzi (co może być stroną HTML, obrazkiem, przekierowaniem itp.).

Zatem na początku przekazywane jest mu żądanie. Nie jest to bezpośrednio żądanie HTTP, ale obiekt Nette\Application\Request, na który zostało przekształcone żądanie HTTP za pomocą routera. Z tym obiektem zazwyczaj nie mamy do czynienia, ponieważ presenter inteligentnie deleguje przetwarzanie żądania do innych metod, które teraz pokażemy.

Cykl życia presentera

Obrazek przedstawia listę metod, które są kolejno wywoływane od góry do dołu, jeśli istnieją. Żadna z nich nie musi istnieć, możemy mieć całkowicie pusty presenter bez ani jednej metody i zbudować na nim prostą statyczną stronę internetową.

__construct()

Konstruktor nie należy tak do końca do cyklu życia presentera, ponieważ jest wywoływany w momencie tworzenia obiektu. Ale podajemy go ze względu na ważność. Konstruktor (wraz z metodą inject) służy do przekazywania zależności.

Presenter nie powinien zajmować się logiką biznesową aplikacji, zapisywać i czytać z bazy danych, wykonywać obliczeń itp. Od tego są klasy z warstwy, którą określamy jako model. Na przykład klasa ArticleRepository może odpowiadać za ładowanie i zapisywanie artykułów. Aby presenter mógł z nią pracować, pozwoli sobie ją przekazać za pomocą dependency injection:

class ArticlePresenter extends Nette\Application\UI\Presenter
{
	public function __construct(
		private ArticleRepository $articles,
	) {
	}
}

startup()

Natychmiast po otrzymaniu żądania wywoływana jest metoda startup(). Można jej użyć do inicjalizacji właściwości, weryfikacji uprawnień użytkownika itp. Wymagane jest, aby metoda zawsze wywoływała przodka parent::startup().

action<Action>(args...)

Odpowiednik metody render<View>(). Podczas gdy render<View>() jest przeznaczona do przygotowania danych dla konkretnego szablonu, który następnie zostanie wyrenderowany, to w action<Action>() przetwarza się żądanie bez związku z renderowaniem szablonu. Na przykład przetwarza się dane, loguje lub wylogowuje użytkownika i tak dalej, a następnie przekierowuje gdzie indziej.

Ważne jest, że action<Action>() jest wywoływana wcześniej niż render<View>(), więc możemy w niej ewentualnie zmienić dalszy bieg wydarzeń, tj. zmienić szablon, który będzie rysowany, a także metodę render<View>(), która będzie wywoływana. A to za pomocą setView('innyView').

Metodzie przekazywane są parametry z żądania. Możliwe i zalecane jest podanie typów parametrów, np. actionShow(int $id, ?string $slug = null) – jeśli parametr id będzie brakował lub jeśli nie będzie liczbą całkowitą, presenter zwróci błąd 404 i zakończy działanie.

handle<Signal>(args...)

Metoda przetwarza tzw. sygnały, z którymi zapoznamy się w rozdziale poświęconym komponentom. Jest bowiem przeznaczona głównie dla komponentów i przetwarzania żądań AJAX.

Metodzie przekazywane są parametry z żądania, jak w przypadku action<Action>(), w tym kontrola typów.

beforeRender()

Metoda beforeRender, jak sama nazwa wskazuje, jest wywoływana przed każdą metodą render<View>(). Używa się jej do wspólnej konfiguracji szablonu, przekazania zmiennych dla layoutu i podobnych.

render<View>(args...)

Miejsce, gdzie przygotowujemy szablon do późniejszego wyrenderowania, przekazujemy mu dane itp.

Metodzie przekazywane są parametry z żądania, jak w przypadku action<Action>(), w tym kontrola typów.

public function renderShow(int $id): void
{
	// pobieramy dane z modelu i przekazujemy do szablonu
	$this->template->article = $this->articles->getById($id);
}

afterRender()

Metoda afterRender, jak nazwa ponownie wskazuje, jest wywoływana po każdej metodzie render<View>(). Używa się jej raczej wyjątkowo.

shutdown()

Wywoływana na końcu cyklu życia presentera.

Dobra rada, zanim pójdziemy dalej. Presenter, jak widać, może obsługiwać więcej akcji/view, czyli mieć więcej metod render<View>(). Ale zalecamy projektowanie presenterów z jedną lub jak najmniejszą liczbą akcji.

Wysłanie odpowiedzi

Odpowiedzią presentera jest zazwyczaj wyrenderowanie szablonu ze stroną HTML, ale może nią być również wysłanie pliku, JSON lub na przykład przekierowanie na inną stronę.

W dowolnym momencie cyklu życia możemy za pomocą jednej z poniższych metod wysłać odpowiedź i jednocześnie zakończyć działanie presentera:

Jeśli nie wywołasz żadnej z tych metod, presenter automatycznie przystąpi do renderowania szablonu. Dlaczego? Ponieważ w 99% przypadków chcemy wyrenderować szablon, dlatego presenter to zachowanie traktuje jako domyślne i chce nam ułatwić pracę.

Tworzenie linków

Presenter dysponuje metodą link(), za pomocą której można tworzyć linki URL do innych presenterów. Pierwszym parametrem jest docelowy presenter & akcja, następnie przekazywane argumenty, które mogą być podane jako tablica:

$url = $this->link('Product:show', $id);

$url = $this->link('Product:show', [$id, 'lang' => 'cs']);

W szablonie tworzy się linki do innych presenterów & akcji w ten sposób:

<a n:href="Product:show $id">szczegóły produktu</a>

Po prostu zamiast rzeczywistego URL wpisujesz znaną parę Presenter:action i podajesz ewentualne parametry. Sztuczka tkwi w n:href, które mówi, że ten atrybut przetworzy Latte i wygeneruje rzeczywisty URL. W Nette więc w ogóle nie musisz zastanawiać się nad URL, tylko nad presenterami i akcjami.

Więcej informacji znajdziesz w rozdziale Tworzenie linków URL.

Przekierowanie

Do przejścia na inny presenter służą metody redirect() i forward(), które mają bardzo podobną składnię jak metoda link().

Metoda forward() przechodzi na nowy presenter natychmiast bez przekierowania HTTP:

$this->forward('Product:show');

Przykład tzw. tymczasowego przekierowania z kodem HTTP 302 (lub 303, jeśli metodą aktualnego żądania jest POST):

$this->redirect('Product:show', $id);

Stałe przekierowanie z kodem HTTP 301 osiągniesz w ten sposób:

$this->redirectPermanent('Product:show', $id);

Na inny URL poza aplikacją można przekierować metodą redirectUrl(). Jako drugi parametr można podać kod HTTP, domyślny to 302 (lub 303, jeśli metodą aktualnego żądania jest POST):

$this->redirectUrl('https://nette.org');

Przekierowanie natychmiast kończy działanie presentera przez wyrzucenie tzw. cichego wyjątku kończącego Nette\Application\AbortException.

Przed przekierowaniem można wysłać flash message, czyli wiadomości, które zostaną po przekierowaniu wyświetlone w szablonie.

Wiadomości flash

Są to wiadomości zazwyczaj informujące o wyniku jakiejś 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 jeszcze przez 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.

Wystarczy wywołać metodę flashMessage() a o przekazanie do szablonu zadba presenter. Pierwszym parametrem jest tekst wiadomości, a opcjonalnym drugim parametrem jej typ (error, warning, info itp.). Metoda flashMessage() zwraca instancję wiadomości flash, której można dodawać dalsze 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}

Błąd 404 i spółka.

Jeśli nie można spełnić żądania, na przykład z powodu, że artykuł, który chcemy wyświetlić, nie istnieje w bazie danych, wyrzucamy błąd 404 metodą error(?string $message = null, int $httpCode = 404).

public function renderShow(int $id): void
{
	$article = $this->articles->getById($id);
	if (!$article) {
		$this->error();
	}
	// ...
}

Kod HTTP błędu można przekazać jako drugi parametr, domyślny to 404. Metoda działa tak, że wyrzuca wyjątek Nette\Application\BadRequestException, po czym Application przekazuje sterowanie do error-presentera. Co jest presenterem, którego zadaniem jest wyświetlenie strony informującej o zaistniałym błędzie. Ustawienie error-preseteru dokonuje się w konfiguracji application.

Wysłanie JSON

Przykład metody action, która wysyła dane w formacie JSON i kończy presenter:

public function actionData(): void
{
	$data = ['hello' => 'nette'];
	$this->sendJson($data);
}

Parametry żądania

Presenter, a także każdy komponent, uzyskuje z żądania HTTP swoje parametry. Ich wartość można uzyskać metodą getParameter($name) lub getParameters(). Wartości są ciągami znaków lub tablicami ciągów znaków, są to w zasadzie surowe dane uzyskane bezpośrednio z URL.

Dla większej wygody zalecamy udostępnianie parametrów przez właściwości. Wystarczy oznaczyć je atrybutem #[Parameter]:

use Nette\Application\Attributes\Parameter;  // ta linia jest ważna

class HomePresenter extends Nette\Application\UI\Presenter
{
	#[Parameter]
	public string $theme; // musi być public
}

Przy właściwości zalecamy podanie również typu danych (np. string), a Nette na jego podstawie automatycznie przeliczy wartość. Wartości parametrów można również walidować.

Przy tworzeniu linku można parametrom wartość ustawić bezpośrednio:

<a n:href="Home:default theme: dark">kliknij</a>

Parametry trwałe

Parametry trwałe służą do utrzymywania stanu 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 przekazywane w URL. I to całkowicie automatycznie, nie trzeba ich więc jawnie podawać w link() lub n:href.

Przykład użycia? Masz aplikację wielojęzyczną. Aktualny język jest parametrem, który musi być stale częścią URL. Ale byłoby niesamowicie męczące podawanie go w każdym linku. Więc zrobisz z niego parametr trwały lang i będzie się przenosił sam. Parada!

Tworzenie parametru trwałego jest w Nette 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 ProductPresenter extends Nette\Application\UI\Presenter
{
	#[Persistent]
	public string $lang; // musi być public
}

Jeśli $this->lang będzie miał wartość na przykład 'en', to również linki utworzone za pomocą link() lub n:href będą zawierać parametr lang=en. A po kliknięciu na link ponownie $this->lang = 'en'.

Przy właściwości zalecamy podanie również typu danych (np. string) i można podać również wartość domyślną. Wartości parametrów można walidować.

Parametry trwałe standardowo przenoszą się między wszystkimi akcjami danego presentera. Aby przenosiły się również między wieloma presenterami, trzeba je zdefiniować albo:

  • we wspólnym przodku, od którego dziedziczą presentery
  • w trait, którego użyją presentery:
trait LanguageAware
{
	#[Persistent]
	public string $lang;
}

class ProductPresenter extends Nette\Application\UI\Presenter
{
	use LanguageAware;
}

Przy tworzeniu linku można parametrowi trwałemu zmienić wartość:

<a n:href="Product:show $id, lang: cs">szczegóły po czesku</a>

Lub można go zresetować, tj. usunąć z URL. Wtedy przyjmie swoją wartość domyślną:

<a n:href="Product:show $id, lang: null">kliknij</a>

Komponenty interaktywne

Presentery mają wbudowany system komponentów. Komponenty to samodzielne, wielokrotnego użytku całości, które wstawiamy do presenterów. Mogą to być formularze, siatki danych, menu, właściwie cokolwiek, co ma sens używać wielokrotnie.

Jak wstawia się komponenty do presentera i następnie używa? Dowiesz się tego w rozdziale Komponenty. Nawet dowiesz się, co mają wspólnego z Hollywoodem.

A gdzie mogę zdobyć komponenty? Na stronie Componette znajdziesz komponenty open-source oraz wiele innych dodatków do Nette, które umieścili tu wolontariusze ze społeczności wokół frameworka.

Idziemy do hloubky

Z tym, co do tej pory pokazaliśmy w tym rozdziale, prawdopodobnie w zupełności sobie poradzisz. Poniższe linijki są przeznaczone dla tych, którzy interesują się presenterami dogłębnie i chcą wiedzieć absolutnie wszystko.

Walidacja parametrów

Wartości parametrów żądaniaparametrów trwałych otrzymanych z URL zapisuje do właściwości metoda loadState(). Ta również kontroluje, czy odpowiada typ danych podany przy właściwości, w przeciwnym razie odpowie błędem 404 i strona się nie wyświetli.

Nigdy ślepo nie wierz parametrom, ponieważ mogą być łatwo przez użytkownika nadpisane w URL. W ten sposób na przykład zweryfikujemy, czy język $this->lang jest wśród wspieranych. Odpowiednią drogą jest nadpisanie wspomnianej metody loadState():

class ProductPresenter extends Nette\Application\UI\Presenter
{
	#[Persistent]
	public string $lang;

	public function loadState(array $params): void
	{
		parent::loadState($params); // tutaj ustawia się $this->lang
		// następuje własna kontrola wartości:
		if (!in_array($this->lang, ['en', 'cs'])) {
			$this->error();
		}
	}
}

Zapisanie i odtworzenie żądania

Żądanie, które obsługuje presenter, jest obiektem Nette\Application\Request i zwraca go metoda presentera getRequest().

Aktualne żądanie można zapisać do sesji lub odwrotnie, odtworzyć z niej i pozwolić presenterowi ponownie je wykonać. Przydaje się to na przykład w sytuacji, gdy użytkownik wypełnia formularz i wygaśnie mu sesja logowania. Aby nie stracił danych, przed przekierowaniem na stronę logowania aktualne żądanie zapisujemy do sesji za pomocą $reqId = $this->storeRequest(), które zwraca jego identyfikator w postaci krótkiego ciągu znaków, a ten przekazujemy jako parametr do presentera logowania.

Po zalogowaniu wywołujemy metodę $this->restoreRequest($reqId), która pobiera żądanie z sesji i forwarduje na nie. Metoda przy tym weryfikuje, czy żądanie utworzył ten sam użytkownik, który się teraz zalogował. Jeśli zalogowałby się inny użytkownik lub klucz byłby nieprawidłowy, nie zrobi nic i program kontynuuje dalej.

Zobacz poradnik Jak wrócić do poprzedniej strony.

Kanonizacja

Presentery mają jedną naprawdę świetną cechę, która przyczynia się do lepszego SEO (optymalizacji dla wyszukiwarek internetowych). Automatycznie zapobiegają istnieniu duplikatów treści pod różnymi URL. Jeśli do określonego celu prowadzi więcej adresów URL, np. /index i /index?page=1, framework określa jeden z nich jako podstawowy (kanoniczny) i pozostałe na niego przekierowuje za pomocą kodu HTTP 301. Dzięki temu wyszukiwarki nie indeksują stron dwukrotnie i nie rozdrabniają ich page rank.

Ten proces nazywa się kanonizacją. Kanonicznym URL jest ten, który generuje router, zazwyczaj więc pierwsza pasująca trasa w kolekcji.

Kanonizacja jest domyślnie włączona i można ją wyłączyć przez $this->autoCanonicalize = false.

Do przekierowania nie dochodzi przy żądaniu AJAX lub POST, ponieważ doszłoby do utraty danych lub nie miałoby to wartości dodanej z punktu widzenia SEO.

Kanonizację można wywołać również manualnie za pomocą metody canonicalize(), której podobnie jak metodzie link() przekazuje się presenter, akcję i parametry. Tworzy link i porównuje go z aktualnym 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);
	// przekierowuje, jeśli $slug różni się od $realSlug
	$this->canonicalize('Product:show', [$id, $realSlug]);
}

Zdarzenia

Oprócz metod startup(), beforeRender() i shutdown(), które są wywoływane jako część cyklu życia presentera, można zdefiniować jeszcze inne funkcje, które mają być automatycznie wywoływane. Presenter definiuje tzw. zdarzenia, których handlery dodasz do tablic $onStartup, $onRender i $onShutdown.

class ArticlePresenter extends Nette\Application\UI\Presenter
{
	public function __construct()
	{
		$this->onStartup[] = function () {
			// ...
		};
	}
}

Handlery w tablicy $onStartup są wywoływane tuż przed metodą startup(), dalej $onRender między beforeRender() a render<View>() i na końcu $onShutdown tuż przed shutdown().

Odpowiedzi

Odpowiedź, którą zwraca presenter, jest obiektem implementującym interfejs Nette\Application\Response. Dostępnych jest szereg gotowych odpowiedzi:

Odpowiedzi wysyła się metodą 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'));

// Odpowiedzią będzie callback
$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 za pomocą #[Requires]

Atrybut #[Requires] zapewnia zaawansowane możliwości ograniczania dostępu do presenterów i ich metod. Można go użyć do specyfikacji metod HTTP, wymagania żądania AJAX, ograniczenia do tego samego pochodzenia (same origin) oraz dostępu tylko przez forwardowanie. Atrybut można stosować zarówno do klas presenterów, jak i do poszczególnych metod action<Action>(), render<View>(), handle<Signal>() i createComponent<Name>().

Można określić te ograniczenia:

  • na metody HTTP: #[Requires(methods: ['GET', 'POST'])]
  • wymaganie żądania AJAX: #[Requires(ajax: true)]
  • dostęp tylko z tego samego pochodzenia: #[Requires(sameOrigin: true)]
  • dostęp tylko przez forward: #[Requires(forward: true)]
  • ograniczenie do konkretnych akcji: #[Requires(actions: 'default')]

Szczegóły znajdziesz w poradniku Jak używać atrybutu Requires.

Kontrola metody HTTP

Presentery w Nette automatycznie weryfikują metodę HTTP każdego przychodzącego żądania. Powodem tej kontroli jest przede wszystkim bezpieczeństwo. Standardowo dozwolone są metody GET, POST, HEAD, PUT, DELETE, PATCH.

Jeśli chcesz dodatkowo zezwolić na przykład na metodę OPTIONS, użyj do tego atrybutu #[Requires] (od 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 odbywa się w checkHttpMethod(), która sprawdza, czy metoda określona w żądaniu jest zawarta w tablicy $presenter->allowedMethods. Dodanie metody wykonaj w ten sposób:

class MyPresenter extends Nette\Application\UI\Presenter
{
    protected function checkHttpMethod(): void
    {
        $this->allowedMethods[] = 'OPTIONS';
        parent::checkHttpMethod();
    }
}

Ważne jest podkreślenie, że jeśli zezwolisz na metodę OPTIONS, musisz ją następnie również odpowiednio obsłużyć w ramach swojego presentera. Metoda jest często używana jako tzw. preflight request, który przeglądarka automatycznie wysyła przed rzeczywistym żądaniem, gdy trzeba sprawdzić, czy żądanie jest dozwolone z punktu widzenia polityki CORS (Cross-Origin Resource Sharing). Jeśli zezwolisz na metodę, ale nie zaimplementujesz prawidłowej odpowiedzi, może to prowadzić do niespójności i potencjalnych problemów bezpieczeństwa.

Dalsza lektura

wersja: 4.0