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.