Tworzenie interfejsu u偶ytkownika
Wszystko jest teraz na swoim miejscu. Mo偶emy stworzy膰 pierwsz膮 wersj臋 interfejsu strony. Nie b臋dziemy rozczula膰 si臋 nad jej wygl膮dem. Na razie skupmy si臋 na jej dzia艂aniu.
Pami臋tasz zabieg, kt贸rego musieli艣my dokona膰 w kontrolerze dla easter egga, aby unikn膮膰 problem贸w z bezpiecze艅stwem? Z tego powodu nie b臋dziemy u偶ywa膰 PHP w naszych szablonach. Zamiast tego u偶yjemy Twiga. Opr贸cz obs艂ugi filtrowania wyj艣cia Twig wnosi wiele przydatnych funkcji, takich jak dziedziczenie szablon贸w.
U偶ywanie Twiga do szablon贸w
Wszystkie podstrony w serwisie b臋d膮 mia艂y ten sam uk艂ad. Podczas instalacji Twiga, katalog templates/
zosta艂 utworzony automatycznie, a w nim utworzono r贸wnie偶 przyk艂adowy uk艂ad w pliku base.html.twig
.
Uk艂ad mo偶e definiowa膰 elementy nazywane blokami (ang. block
). S膮 to miejsca, kt贸re pozwalaj膮 na rozszerzenie uk艂adu, dodaj膮c do niego zawarto艣膰 z szablonu potomnego.
Stw贸rzmy szablon dla strony g艂贸wnej naszego projektu w pliku templates/conference/index.html.twig
:
Szablon rozszerza base.html.twig
i nadpisuje bloki body
oraz title
.
Zapis {% %}
w szablonie wskazuje dzia艂ania i struktur臋.
Zapis {{ }}
s艂u偶y do wy艣wietlania warto艣ci. {{ conference }}
wy艣wietli reprezentacj臋 obiektu (wynik wywo艂ania metody __toString
na obiekcie klasy Conference
).
U偶ywanie Twiga w kontrolerze
Zaktualizuj kontroler, aby wyrenderowa膰 szablon Twiga:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
--- a/src/Controller/ConferenceController.php
+++ b/src/Controller/ConferenceController.php
@@ -2,22 +2,19 @@
namespace App\Controller;
+use App\Repository\ConferenceRepository;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
+use Twig\Environment;
class ConferenceController extends AbstractController
{
#[Route('/', name: 'homepage')]
- public function index(): Response
+ public function index(Environment $twig, ConferenceRepository $conferenceRepository): Response
{
- return new Response(<<<EOF
-<html>
- <body>
- <img src="/images/under-construction.gif" />
- </body>
-</html>
-EOF
- );
+ return new Response($twig->render('conference/index.html.twig', [
+ 'conferences' => $conferenceRepository->findAll(),
+ ]));
}
}
Sporo si臋 tu dzieje.
Aby m贸c wyrenderowa膰 szablon, potrzebujemy obiektu Environment
z biblioteki Twig (kluczowego elementu tej biblioteki). Zauwa偶, 偶e odnosimy si臋 do instancji klasy Twig, wskazuj膮c j膮 w metodzie kontrolera. Symfony jest na tyle inteligentny, 偶e wie, jak wstrzykn膮膰 odpowiedni obiekt.
Potrzebujemy r贸wnie偶 repozytorium konferencji, aby mie膰 mo偶liwo艣膰 pobrania wszystkich konferencji z bazy danych.
W kodzie kontrolera mamy metod臋 render()
, kt贸ra renderuje szablon i przekazuje do niego tablic臋 zmiennych. Przekazujemy list臋 obiekt贸w klasy Conference
jako zmienn膮 conferences
.
Kontroler jest zwyk艂膮 klas膮 PHP. Nie musi ona nawet dziedziczy膰 po klasie AbstractController
, je艣li nie chcemy mie膰 zbyt wielu zale偶no艣ci. Mo偶esz usun膮膰 to dziedziczenie (ale nie r贸b tego, poniewa偶 w kolejnych krokach skorzystamy z uproszcze艅, kt贸re daje nam ten zabieg).
Tworzenie strony konferencji
Dla ka偶dej konferencji powinni艣my mie膰 osobn膮 stron臋, na kt贸rej mo偶na wy艣wietli膰 list臋 komentarzy. Dodanie nowej strony sprowadza si臋 do stworzenia kontrolera, zdefiniowania dla niego trasy (ang. route) i utworzenia odpowiedniego szablonu.
Dodaj metod臋 show()
w src/Controller/ConferenceController.php
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
--- a/src/Controller/ConferenceController.php
+++ b/src/Controller/ConferenceController.php
@@ -2,6 +2,8 @@
namespace App\Controller;
+use App\Entity\Conference;
+use App\Repository\CommentRepository;
use App\Repository\ConferenceRepository;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
@@ -17,4 +19,13 @@ class ConferenceController extends AbstractController
'conferences' => $conferenceRepository->findAll(),
]));
}
+
+ #[Route('/conference/{id}', name: 'conference')]
+ public function show(Environment $twig, Conference $conference, CommentRepository $commentRepository): Response
+ {
+ return new Response($twig->render('conference/show.html.twig', [
+ 'conference' => $conference,
+ 'comments' => $commentRepository->findBy(['conference' => $conference], ['createdAt' => 'DESC']),
+ ]));
+ }
}
Ta metoda zachowuje si臋 w szczeg贸lny spos贸b, z kt贸rym jeszcze si臋 nie spotkali艣my. Prosimy o wstrzykni臋cie do niej obiektu klasy Conference
. Konferencji w bazie mo偶e by膰 wiele, ale Symfony jest w stanie okre艣li膰, kt贸r膮 z nich chcesz pobra膰, na podstawie parametru {id}
podanego w 艣cie偶ce 偶膮dania (id
jest kluczem podstawowym tabeli conference
w bazie danych).
Pobranie komentarzy powi膮zanych z konferencj膮 mo偶na wykona膰 za pomoc膮 metody findBy()
, kt贸ra przyjmuje kryteria zapytania jako pierwszy argument.
Ostatnim krokiem jest utworzenie pliku templates/conference/show.html.twig
:
W tym szablonie u偶ywamy zapisu |
do wywo艂ywania filtr贸w Twiga. Filtr zajmuje si臋 przekszta艂caniem podanej warto艣ci. comments|length
zwraca liczb臋 komentarzy, a comment.createdAt|format_datetime('medium', 'short')
wy艣wietla dat臋 w czytelnym dla cz艂owieka formacie.
Pr贸buj膮c dotrze膰 do pierwszej konferencji poprzez adres /conference/1
zauwa偶ysz nast臋puj膮cy b艂膮d:
B艂膮d jest efektem wywo艂ania format_datetime
poniewa偶 ten filtr nie jest cz臋艣ci膮 Twiga. Komunikat o b艂臋dzie wskazuje, kt贸ry pakiet powinien zosta膰 zainstalowany, aby rozwi膮za膰 problem:
1
$ symfony composer req "twig/intl-extra:^3"
Teraz strona dzia艂a poprawnie.
Tworzenie odno艣nik贸w mi臋dzy stronami
Ostatnim krokiem niezb臋dnym do uko艅czenia naszej pierwszej wersji interfejsu u偶ytkownika jest stworzenie odno艣nik贸w na stronie g艂贸wnej do stron poszczeg贸lnych konferencji:
1 2 3 4 5 6 7 8 9 10 11
--- a/templates/conference/index.html.twig
+++ b/templates/conference/index.html.twig
@@ -7,5 +7,8 @@
{% for conference in conferences %}
<h4>{{ conference }}</h4>
+ <p>
+ <a href="/conference/{{ conference.id }}">View</a>
+ </p>
{% endfor %}
{% endblock %}
Zapisywanie 艣cie偶ki na sztywno w kodzie jest nie najlepszym pomys艂em z kilku powod贸w. Najwa偶niejszym z nim jest sytuacja, w kt贸rej zmieniaj膮c 艣cie偶k臋 (np. z /conference/{id}
na /conferences/{id}
), musimy poprawi膰 r臋cznie wszystkie odno艣niki.
Zamiast wpisywania 艣cie偶ki na sztywno w kodzie, u偶yj wbudowanej funkcji Twiga path()
i nazwy trasy :
1 2 3 4 5 6 7 8 9 10 11
--- a/templates/conference/index.html.twig
+++ b/templates/conference/index.html.twig
@@ -8,7 +8,7 @@
{% for conference in conferences %}
<h4>{{ conference }}</h4>
<p>
- <a href="/conference/{{ conference.id }}">View</a>
+ <a href="{{ path('conference', { id: conference.id }) }}">View</a>
</p>
{% endfor %}
{% endblock %}
Funkcja path()
tworzy 艣cie偶k臋 do strony na podstawie nazwy trasy. Warto艣ci parametr贸w 艣cie偶ki s膮 przekazywane jako argumenty Twiga.
Stronicowanie komentarzy
Przy tysi膮cach uczestnik贸w mo偶emy spodziewa膰 si臋 wielu komentarzy. Je艣li wy艣wietlimy je wszystkie na jednej stronie, to b臋dzie ona si臋 wyd艂u偶a膰 w zastraszaj膮cym tempie.
Stw贸rz metod臋 getCommentPaginator()
w repozytorium komentarzy, kt贸ra zwr贸ci obiekt do stronicowania komentarzy (ang. paginator) na podstawie konferencji i przesuni臋cia (ang. offset), czyli informacji, od kt贸rego komentarza nale偶y zacz膮膰:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41
--- a/src/Repository/CommentRepository.php
+++ b/src/Repository/CommentRepository.php
@@ -3,8 +3,10 @@
namespace App\Repository;
use App\Entity\Comment;
+use App\Entity\Conference;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Persistence\ManagerRegistry;
+use Doctrine\ORM\Tools\Pagination\Paginator;
/**
* @method Comment|null find($id, $lockMode = null, $lockVersion = null)
@@ -14,11 +16,27 @@ use Doctrine\Persistence\ManagerRegistry;
*/
class CommentRepository extends ServiceEntityRepository
{
+ public const PAGINATOR_PER_PAGE = 2;
+
public function __construct(ManagerRegistry $registry)
{
parent::__construct($registry, Comment::class);
}
+ public function getCommentPaginator(Conference $conference, int $offset): Paginator
+ {
+ $query = $this->createQueryBuilder('c')
+ ->andWhere('c.conference = :conference')
+ ->setParameter('conference', $conference)
+ ->orderBy('c.createdAt', 'DESC')
+ ->setMaxResults(self::PAGINATOR_PER_PAGE)
+ ->setFirstResult($offset)
+ ->getQuery()
+ ;
+
+ return new Paginator($query);
+ }
+
// /**
// * @return Comment[] Returns an array of Comment objects
// */
Aby u艂atwi膰 testowanie, ustalili艣my maksymaln膮 liczb臋 komentarzy na stron臋 na 2 wpisy.
Aby m贸c zmienia膰 stronicowanie w szablonie, zamiast obiektu Doctrine Collection przeka偶 do Twiga obiekt Doctrine Paginator:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
--- a/src/Controller/ConferenceController.php
+++ b/src/Controller/ConferenceController.php
@@ -6,6 +6,7 @@ use App\Entity\Conference;
use App\Repository\CommentRepository;
use App\Repository\ConferenceRepository;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
+use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
use Twig\Environment;
@@ -21,11 +22,16 @@ class ConferenceController extends AbstractController
}
#[Route('/conference/{id}', name: 'conference')]
- public function show(Environment $twig, Conference $conference, CommentRepository $commentRepository): Response
+ public function show(Request $request, Environment $twig, Conference $conference, CommentRepository $commentRepository): Response
{
+ $offset = max(0, $request->query->getInt('offset', 0));
+ $paginator = $commentRepository->getCommentPaginator($conference, $offset);
+
return new Response($twig->render('conference/show.html.twig', [
'conference' => $conference,
- 'comments' => $commentRepository->findBy(['conference' => $conference], ['createdAt' => 'DESC']),
+ 'comments' => $paginator,
+ 'previous' => $offset - CommentRepository::PAGINATOR_PER_PAGE,
+ 'next' => min(count($paginator), $offset + CommentRepository::PAGINATOR_PER_PAGE),
]));
}
}
Kontroler pobiera parametr offset
z adresu 偶膮dania przechowywanego w obiekcie klasy Request ($request->query
) jako liczb臋 ca艂kowit膮 (getInt()
). Domy艣lnie jest to 0, je艣li parametr ten nie zosta艂 podany.
W艂a艣ciwo艣ci previous
i next
(przesuni臋cie do przodu i do ty艂u) s膮 okre艣lane na podstawie wszystkich informacji pochodz膮cych z paginatora.
Na zako艅czenie, zaktualizuj szablon dodaj膮c odno艣niki do nast臋pnej i poprzedniej strony:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
--- a/templates/conference/show.html.twig
+++ b/templates/conference/show.html.twig
@@ -6,6 +6,8 @@
<h2>{{ conference }} Conference</h2>
{% if comments|length > 0 %}
+ <div>There are {{ comments|length }} comments.</div>
+
{% for comment in comments %}
{% if comment.photofilename %}
<img src="{{ asset('uploads/photos/' ~ comment.photofilename) }}" />
@@ -18,6 +20,13 @@
<p>{{ comment.text }}</p>
{% endfor %}
+
+ {% if previous >= 0 %}
+ <a href="{{ path('conference', { id: conference.id, offset: previous }) }}">Previous</a>
+ {% endif %}
+ {% if next < comments|length %}
+ <a href="{{ path('conference', { id: conference.id, offset: next }) }}">Next</a>
+ {% endif %}
{% else %}
<div>No comments have been posted yet for this conference.</div>
{% endif %}
Teraz mo偶esz porusza膰 si臋 po komentarzach za pomoc膮 odno艣nik贸w "Poprzedni" i "Nast臋pny":
Refaktoryzowanie kontrolera
By膰 mo偶e uda艂o Ci si臋 zauwa偶y膰, 偶e obie metody w ConferenceController
jako argument przyjmuj膮 obiekt Environment biblioteki Twig. Zamiast wstrzykiwa膰 ten obiekt do ka偶dej z metod osobno, u偶yjmy wstrzykni臋cia go do konstruktora (sprawia to, 偶e lista argument贸w metod jest kr贸tsza):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35
--- a/src/Controller/ConferenceController.php
+++ b/src/Controller/ConferenceController.php
@@ -13,21 +13,28 @@ use Twig\Environment;
class ConferenceController extends AbstractController
{
+ private $twig;
+
+ public function __construct(Environment $twig)
+ {
+ $this->twig = $twig;
+ }
+
#[Route('/', name: 'homepage')]
- public function index(Environment $twig, ConferenceRepository $conferenceRepository): Response
+ public function index(ConferenceRepository $conferenceRepository): Response
{
- return new Response($twig->render('conference/index.html.twig', [
+ return new Response($this->twig->render('conference/index.html.twig', [
'conferences' => $conferenceRepository->findAll(),
]));
}
#[Route('/conference/{id}', name: 'conference')]
- public function show(Request $request, Environment $twig, Conference $conference, CommentRepository $commentRepository): Response
+ public function show(Request $request, Conference $conference, CommentRepository $commentRepository): Response
{
$offset = max(0, $request->query->getInt('offset', 0));
$paginator = $commentRepository->getCommentPaginator($conference, $offset);
- return new Response($twig->render('conference/show.html.twig', [
+ return new Response($this->twig->render('conference/show.html.twig', [
'conference' => $conference,
'comments' => $paginator,
'previous' => $offset - CommentRepository::PAGINATOR_PER_PAGE,
Id膮c dalej