Nette Documentation Preview

syntax
Strona pojedynczego wpisu
*************************

.[perex]
Dodajmy do naszego bloga kolejną stronę, która będzie wyświetlać treść jednego konkretnego wpisu.


Musimy utworzyć nową metodę render, która pobierze jeden konkretny wpis i przekaże go do szablonu. Umieszczenie tej metody w `HomePresenter` nie jest zbyt eleganckie, bo chodzi o pojedynczy wpis, a nie o stronę główną. Utwórzmy więc nową klasę `PostPresenter` i umieśćmy ją w `app/Presentation/Post/`. Ten presenter też będzie potrzebował połączenia z bazą danych, więc dodamy konstruktor, który będzie tego połączenia wymagał.

`PostPresenter` mógłby wyglądać tak:

```php .{file:app/Presentation/Post/PostPresenter.php}
<?php
namespace App\Presentation\Post;

use Nette;
use Nette\Application\UI\Form;

final class PostPresenter extends Nette\Application\UI\Presenter
{
	public function __construct(
		private Nette\Database\Explorer $database,
	) {
	}

	public function renderShow(int $id): void
	{
		$this->template->post = $this->database
			->table('posts')
			->get($id);
	}
}
```

Nie możemy zapomnieć o podaniu poprawnej przestrzeni nazw `App\Presentation\Post`, która zależy od konfiguracji [mapowania presenterów |https://github.com/nette-examples/quickstart/blob/v4.0/config/common.neon#L6-L7].

Metoda `renderShow` wymaga jednego argumentu: ID wpisu, który ma zostać wyświetlony. Następnie wczytuje wpis z bazy danych i przekazuje go do szablonu.

W szablonie `Home/default.latte` dodajemy odnośnik do akcji `Post:show`:

```latte .{file:app/Presentation/Home/default.latte}
...
<h2><a href="{link Post:show $post->id}">{$post->title}</a></h2>
...
```

Tag `{link}` generuje adres URL wskazujący na akcję `Post:show`. Przekazuje też jako argument ID wpisu.


To samo można zapisać zwięźle za pomocą n:atrybutu:

```latte .{file:app/Presentation/Home/default.latte}
...
<h2><a n:href="Post:show $post->id">{$post->title}</a></h2>
...
```

Atrybut `n:href` jest podobny do tagu `{link}`.



Szablon dla akcji `Post:show` jednak jeszcze nie istnieje. Możemy spróbować otworzyć odnośnik do tego wpisu. [Tracy |tracy:] pokaże błąd, bo szablon `Post/show.latte` jeszcze nie istnieje. Jeśli widzisz inny komunikat o błędzie, prawdopodobnie musisz włączyć na swoim serwerze webowym `mod_rewrite`.

Utworzymy więc `Post/show.latte` z taką treścią:

```latte .{file:app/Presentation/Post/show.latte}
{block content}

<p><a n:href="Home:default">← powrót do listy wpisów</a></p>

<div class="date">{$post->created_at|date:'F j, Y'}</div>

<h1 n:block="title">{$post->title}</h1>

<div class="post">{$post->content}</div>
```

Przejrzyjmy teraz poszczególne części szablonu.

Pierwsza linia zaczyna definicję bloku o nazwie "content", tak samo jak na stronie głównej. Blok ten znów zostanie wyświetlony w ramach głównego szablonu layoutu. Jak widzisz, brakuje tagu końcowego `{/block}`. Jest on opcjonalny.

Druga linia daje odnośnik powrotny do listy wpisów blogowych, pozwalając użytkownikowi łatwo poruszać się między listą wpisów a konkretnym wpisem. Znów używamy atrybutu `n:href`, więc o wygenerowanie URL zadba Nette. Odnośnik wskazuje na akcję `default` presentera `Home` (mógłbyś napisać też `n:href="Home:"`, bo nazwę akcji `default` można pominąć, zostanie dodana automatycznie).

Trzecia linia formatuje datę filtrem, który już znamy.

Czwarta linia wyświetla *tytuł* wpisu blogowego wewnątrz tagu HTML `<h1>`. Tag ten zawiera atrybut, którego możesz nie rozpoznawać (`n:block="title"`). Zgadniesz, co robi? Jeśli uważnie przeczytałeś poprzednią sekcję, już wiesz, że to `n:atrybut`. To kolejny przykład, równoważny z:

```latte
{block title}<h1>{$post->title}</h1>{/block}
```

Upraszczając, blok ten redefiniuje blok o nazwie `title`. Blok ten jest już zdefiniowany w głównym szablonie *layoutu* (`/app/Presentation/@layout.latte:11`) i podobnie jak nadpisywanie metod w OOP nadpisuje ten z szablonu głównego. `<title>` strony będzie więc teraz zawierać tytuł wyświetlanego wpisu, a wystarczył nam do tego prosty atrybut `n:block="title"`. Świetne, prawda?

Piąta i ostatnia linia szablonu wyświetla pełną treść konkretnego wpisu.


Sprawdzanie ID wpisu
====================

Co się stanie, jeśli ktoś zmieni ID w URL i wstawi nieistniejące `id`? Powinniśmy pokazać użytkownikowi ładny błąd "strona nie znaleziona". Zmodyfikujmy nieco metodę render w `PostPresenter`:

```php .{file:app/Presentation/Post/PostPresenter.php}
public function renderShow(int $id): void
{
	$post = $this->database
		->table('posts')
		->get($id);
	if (!$post) {
		$this->error('Post not found');
	}

	$this->template->post = $post;
}
```

Jeśli wpisu nie da się znaleźć, wywołanie `$this->error(...)` wyświetli stronę 404 ze zrozumiałym komunikatem. Zwróć uwagę, że w środowisku deweloperskim (na localhoście) tej strony błędu nie zobaczysz. Zamiast niej Tracy pokaże wyjątek z pełnymi szczegółami, co jest przy tworzeniu całkiem wygodne. Jeśli chcesz przetestować oba tryby, po prostu zmień argument przekazywany metodzie `setDebugMode` w `Bootstrap.php`.


Podsumowanie
============

Mamy bazę danych z wpisami i aplikację webową z dwoma widokami: pierwszy wyświetla przegląd wszystkich wpisów, a drugi jeden konkretny wpis.

{{priority: -1}}

Strona pojedynczego wpisu

Dodajmy do naszego bloga kolejną stronę, która będzie wyświetlać treść jednego konkretnego wpisu.

Musimy utworzyć nową metodę render, która pobierze jeden konkretny wpis i przekaże go do szablonu. Umieszczenie tej metody w HomePresenter nie jest zbyt eleganckie, bo chodzi o pojedynczy wpis, a nie o stronę główną. Utwórzmy więc nową klasę PostPresenter i umieśćmy ją w app/Presentation/Post/. Ten presenter też będzie potrzebował połączenia z bazą danych, więc dodamy konstruktor, który będzie tego połączenia wymagał.

PostPresenter mógłby wyglądać tak:

<?php
namespace App\Presentation\Post;

use Nette;
use Nette\Application\UI\Form;

final class PostPresenter extends Nette\Application\UI\Presenter
{
	public function __construct(
		private Nette\Database\Explorer $database,
	) {
	}

	public function renderShow(int $id): void
	{
		$this->template->post = $this->database
			->table('posts')
			->get($id);
	}
}

Nie możemy zapomnieć o podaniu poprawnej przestrzeni nazw App\Presentation\Post, która zależy od konfiguracji mapowania presenterów.

Metoda renderShow wymaga jednego argumentu: ID wpisu, który ma zostać wyświetlony. Następnie wczytuje wpis z bazy danych i przekazuje go do szablonu.

W szablonie Home/default.latte dodajemy odnośnik do akcji Post:show:

...
<h2><a href="{link Post:show $post->id}">{$post->title}</a></h2>
...

Tag {link} generuje adres URL wskazujący na akcję Post:show. Przekazuje też jako argument ID wpisu.

To samo można zapisać zwięźle za pomocą n:atrybutu:

...
<h2><a n:href="Post:show $post->id">{$post->title}</a></h2>
...

Atrybut n:href jest podobny do tagu {link}.

Szablon dla akcji Post:show jednak jeszcze nie istnieje. Możemy spróbować otworzyć odnośnik do tego wpisu. Tracy pokaże błąd, bo szablon Post/show.latte jeszcze nie istnieje. Jeśli widzisz inny komunikat o błędzie, prawdopodobnie musisz włączyć na swoim serwerze webowym mod_rewrite.

Utworzymy więc Post/show.latte z taką treścią:

{block content}

<p><a n:href="Home:default">← powrót do listy wpisów</a></p>

<div class="date">{$post->created_at|date:'F j, Y'}</div>

<h1 n:block="title">{$post->title}</h1>

<div class="post">{$post->content}</div>

Przejrzyjmy teraz poszczególne części szablonu.

Pierwsza linia zaczyna definicję bloku o nazwie „content“, tak samo jak na stronie głównej. Blok ten znów zostanie wyświetlony w ramach głównego szablonu layoutu. Jak widzisz, brakuje tagu końcowego {/block}. Jest on opcjonalny.

Druga linia daje odnośnik powrotny do listy wpisów blogowych, pozwalając użytkownikowi łatwo poruszać się między listą wpisów a konkretnym wpisem. Znów używamy atrybutu n:href, więc o wygenerowanie URL zadba Nette. Odnośnik wskazuje na akcję default presentera Home (mógłbyś napisać też n:href="Home:", bo nazwę akcji default można pominąć, zostanie dodana automatycznie).

Trzecia linia formatuje datę filtrem, który już znamy.

Czwarta linia wyświetla tytuł wpisu blogowego wewnątrz tagu HTML <h1>. Tag ten zawiera atrybut, którego możesz nie rozpoznawać (n:block="title"). Zgadniesz, co robi? Jeśli uważnie przeczytałeś poprzednią sekcję, już wiesz, że to n:atrybut. To kolejny przykład, równoważny z:

{block title}<h1>{$post->title}</h1>{/block}

Upraszczając, blok ten redefiniuje blok o nazwie title. Blok ten jest już zdefiniowany w głównym szablonie layoutu (/app/Presentation/@layout.latte:11) i podobnie jak nadpisywanie metod w OOP nadpisuje ten z szablonu głównego. <title> strony będzie więc teraz zawierać tytuł wyświetlanego wpisu, a wystarczył nam do tego prosty atrybut n:block="title". Świetne, prawda?

Piąta i ostatnia linia szablonu wyświetla pełną treść konkretnego wpisu.

Sprawdzanie ID wpisu

Co się stanie, jeśli ktoś zmieni ID w URL i wstawi nieistniejące id? Powinniśmy pokazać użytkownikowi ładny błąd „strona nie znaleziona“. Zmodyfikujmy nieco metodę render w PostPresenter:

public function renderShow(int $id): void
{
	$post = $this->database
		->table('posts')
		->get($id);
	if (!$post) {
		$this->error('Post not found');
	}

	$this->template->post = $post;
}

Jeśli wpisu nie da się znaleźć, wywołanie $this->error(...) wyświetli stronę 404 ze zrozumiałym komunikatem. Zwróć uwagę, że w środowisku deweloperskim (na localhoście) tej strony błędu nie zobaczysz. Zamiast niej Tracy pokaże wyjątek z pełnymi szczegółami, co jest przy tworzeniu całkiem wygodne. Jeśli chcesz przetestować oba tryby, po prostu zmień argument przekazywany metodzie setDebugMode w Bootstrap.php.

Podsumowanie

Mamy bazę danych z wpisami i aplikację webową z dwoma widokami: pierwszy wyświetla przegląd wszystkich wpisów, a drugi jeden konkretny wpis.