Skip to content
  • About
    • What is Symfony?
    • Community
    • News
    • Contributing
    • Support
  • Documentation
    • Symfony Docs
    • Symfony Book
    • Screencasts
    • Symfony Bundles
    • Symfony Cloud
    • Training
  • Services
    • Platform.sh for Symfony Best platform to deploy Symfony apps
    • SymfonyInsight Automatic quality checks for your apps
    • Symfony Certification Prove your knowledge and boost your career
    • SensioLabs Professional services to help you with Symfony
    • Blackfire Profile and monitor performance of your apps
  • Other
  • Blog
  • Download
sponsored by
  1. Home
  2. Documentation
  3. Symfony: The Fast Track
  4. Ukrainian
  5. Створення інтерфейсу користувача

Створення інтерфейсу користувача

Тепер все готово для створення першої версії інтерфейсу користувача веб-сайту. Ми не будемо робити його красивим. Для початку, зробимо його функціональним.

Пам’ятаєте як нам довелося екранувати рядок у контролері, щоб уникнути проблем з безпекою, коли ми робили пасхалку? Ми не будемо використовувати PHP для наших шаблонів з цієї ж причини. Натомість будемо використовувати Twig. Окрім безпечної обробки даних, Twig надає ще багато корисних можливостей, які ми будемо використовувати (наприклад, наслідування шаблонів).

Використання Twig для шаблонів

Усі сторінки на веб-сайті матимуть однаковий макет. Під час встановлення Twig автоматично створюється каталог templates/, а також зразок макета base.html.twig.

templates/base.html.twig
1
2
3
4
5
6
7
8
9
10
11
12
<!DOCTYPE html>
<html>
    <head>
        <meta charset="UTF-8">
        <title>{% block title %}Welcome!{% endblock %}</title>
        {% block stylesheets %}{% endblock %}
    </head>
    <body>
        {% block body %}{% endblock %}
        {% block javascripts %}{% endblock %}
    </body>
</html>

У макеті можуть бути визначені спеціальні елементи block, які є місцями, де дочірні шаблони, які наслідують макет, додають власний вміст.

Створімо шаблон для головної сторінки проекту у файлі templates/conference/index.html.twig:

templates/conference/index.html.twig
1
2
3
4
5
6
7
8
9
10
11
{% extends 'base.html.twig' %}

{% block title %}Conference Guestbook{% endblock %}

{% block body %}
    <h2>Give your feedback!</h2>

    {% for conference in conferences %}
        <h4>{{ conference }}</h4>
    {% endfor %}
{% endblock %}

Шаблон наслідує base.html.twig і перевизначає блоки title та body.

Позначення {% %} у шаблоні вказує на дії та структуру.

Позначення {{ }} використовується для відображення чого-небудь. {{ conference }} відображає рядкове представлення конференції (результат виклику __toString для об'єкта Conference).

Використання Twig у контролері

Оновіть контролер, щоб відмалювати шаблон Twig:

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(),
+        ]));
     }
 }

Тут багато чого відбувається.

Щоб відмалювати шаблон, нам потрібен об’єкт Twig — Environment (головна точка входу Twig). Зверніть увагу, щоб отримати екземпляр Twig, достатньо вказати його тип в аргументах методу контролера. Symfony достатньо розумний і знає як автоматично впровадити залежність потрібного типу.

Нам також потрібен репозиторій конференцій, щоб отримати всі конференції з бази даних.

У коді контролера метод render() відмальовує шаблон і передає йому масив змінних. Ми передаємо список об'єктів Conference у якості змінної conferences.

Контролер — це звичайний клас PHP. Нам навіть не потрібно наслідувати клас AbstractController, якщо ми хочемо мати найбільш явний контроль залежностей. Ви можете видалити його (але не робіть цього, оскільки ми будемо використовувати деякі його методи, на наступних кроках).

Створення сторінки для конференції

Кожна конференція має мати власну сторінку з коментарями. Додавання нової сторінки — це додавання контролера, визначення маршруту для нього і створення відповідного шаблону.

Додайте метод show() у 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']),
+        ]));
+    }
 }

Цей метод має особливу поведінку, яку ми ще не бачили. Ми вказуємо, що екземпляр Conference має бути впроваджений у метод. Проте, в базі даних може бути багато об'єктів. Symfony може визначити, який саме вам потрібен, ґрунтуючись на {id}, переданому в рядку запиту (id є первинним ключем таблиці conference в базі даних).

Отримати коментарі, що стосуються конференції, можна за допомогою методу findBy(), який приймає критерії у якості першого аргументу.

Останнім кроком є створення файлу templates/conference/show.html.twig:

templates/conference/show.html.twig
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
{% extends 'base.html.twig' %}

{% block title %}Conference Guestbook - {{ conference }}{% endblock %}

{% block body %}
    <h2>{{ conference }} Conference</h2>

    {% if comments|length > 0 %}
        {% for comment in comments %}
            {% if comment.photofilename %}
                <img src="{{ asset('uploads/photos/' ~ comment.photofilename) }}" />
            {% endif %}

            <h4>{{ comment.author }}</h4>
            <small>
                {{ comment.createdAt|format_datetime('medium', 'short') }}
            </small>

            <p>{{ comment.text }}</p>
        {% endfor %}
    {% else %}
        <div>No comments have been posted yet for this conference.</div>
    {% endif %}
{% endblock %}

У цьому шаблоні ми використовуємо позначення |, щоб викликати фільтри Twig. Фільтр перетворює значення. Наприклад, comments|length повертає кількість коментарів, а comment.createdAt|format_datetime('medium', 'short') форматує дату в читабельне представлення.

Спробуйте відкрити сторінку "першої" конференції за шляхом /conference/1 ​​і зверніть увагу на наступну помилку:

/conference/1

Причиною помилки є використання фільтра format_datetime, оскільки він не є частиною ядра Twig. Повідомлення про помилку дає вам підказку про те, який пакет слід встановити, щоб усунути проблему:

1
$ symfony composer req "twig/intl-extra:^3"

Тепер сторінка працює належним чином.

Перелінкування сторінок

Останнім кроком до завершення першої версії користувацького інтерфейсу є створення посилань на конференції з головної сторінки:

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 %}

Але жорстке визначення шляху є поганою ідеєю з кількох причин. Найважливіша з них полягає в тому, що якщо ви зміните шлях (наприклад, з /conference/{id} на /conferences/{id}), всі посилання доведеться змінювати вручну.

Натомість використовуйте функцію Twig path() і ім'я маршруту:

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 %}

Функція path() генерує шлях до сторінки, використовуючи ім'я її маршруту. Значення параметрів маршруту передаються у вигляді об'єкта Twig.

Пагінація коментарів

З тисячами відвідувачів ми можемо очікувати досить багато коментарів. Якщо ми будемо відображати їх на одній сторінці, вона буде дуже швидко рости.

Створіть метод getCommentPaginator() у репозиторії коментарів, який повертає пагінатор коментарів на основі конференції й зміщення (відносно початку):

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
     //  */

Ми встановили максимальну кількість коментарів на сторінці рівним 2, щоб полегшити тестування.

Щоб керувати пагінацією в шаблоні, передайте пагінатор Doctrine замість колекції Doctrine в Twig:

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),
         ]));
     }
 }

Контролер отримує offset з рядка запиту ($request->query) у вигляді цілого числа (getInt()), за замовчуванням рівного 0, якщо значення відсутнє.

Зміщення previous і next обчислюються на основі всієї інформації, яку ми отримали з пагінатора.

Нарешті, оновіть шаблон, щоб додати посилання на наступну та попередню сторінки:

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 %}

Тепер ви зможете переміщуватися по коментарях через посилання "Previous" та "Next":

/conference/1
/conference/1?offset=2

Рефакторинг контролера

Можливо ви помітили, що обидва методи в ConferenceController отримують середовище Twig у якості аргументу. Замість того щоб додавати його в кожен метод, використовуймо можливості конструктора (що робить список аргументів коротшим і менш громіздким):

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,

Йдемо далі

  • Документація по Twig;
  • Створення й використання шаблонів у застосунках Symfony;
  • Навчальний посібник SymfonyCasts: Twig;
  • Функції та фільтри Twig, які доступні лише в Symfony;
  • Базовий контролер AbstractController.
Previous page Налаштування панелі керування
Next page Розгалуження коду
This work, including the code samples, is licensed under a Creative Commons BY-NC-SA 4.0 license.
TOC
    Version

    Symfony 5.4 is backed by

    Symfony Code Performance Profiling

    Symfony Code Performance Profiling

    Be trained by SensioLabs experts (2 to 6 day sessions -- French or English).

    Be trained by SensioLabs experts (2 to 6 day sessions -- French or English).

    Version:
    Locale:
    ebook

    This book is backed by:

    see all backers

    Symfony footer

    Avatar of botbotbot, a Symfony contributor

    Thanks botbotbot for being a Symfony contributor

    1 commit • 10 lines changed

    View all contributors that help us make Symfony

    Become a Symfony contributor

    Be an active part of the community and contribute ideas, code and bug fixes. Both experts and newcomers are welcome.

    Learn how to contribute

    Symfony™ is a trademark of Symfony SAS. All rights reserved.

    • What is Symfony?

      • What is Symfony?
      • Symfony at a Glance
      • Symfony Components
      • Symfony Releases
      • Security Policy
      • Logo & Screenshots
      • Trademark & Licenses
      • symfony1 Legacy
    • Learn Symfony

      • Symfony Docs
      • Symfony Book
      • Reference
      • Bundles
      • Best Practices
      • Training
      • eLearning Platform
      • Certification
    • Screencasts

      • Learn Symfony
      • Learn PHP
      • Learn JavaScript
      • Learn Drupal
      • Learn RESTful APIs
    • Community

      • Symfony Community
      • SymfonyConnect
      • Events & Meetups
      • Projects using Symfony
      • Contributors
      • Symfony Jobs
      • Backers
      • Code of Conduct
      • Downloads Stats
      • Support
    • Blog

      • All Blog Posts
      • A Week of Symfony
      • Case Studies
      • Cloud
      • Community
      • Conferences
      • Diversity
      • Living on the edge
      • Releases
      • Security Advisories
      • Symfony Insight
      • Twig
      • SensioLabs Blog
    • Services

      • SensioLabs services
      • Train developers
      • Manage your project quality
      • Improve your project performance
      • Host Symfony projects

      Powered by

    Follow Symfony