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:

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ę.

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 żądaniaparametró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:

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.

Dalsza lektura

wersja: 4.0