步骤 12: 监听事件

当前的布局缺少一个导航的页头,这个页头可以用来返回首页,或者从一个会议跳到下一个会议。

添加网站页头

任何会展示在所有页面的内容,比如页头,都是基础布局的一部分:

patch_file

  1. --- a/templates/base.html.twig
  2. +++ b/templates/base.html.twig
  3. @@ -8,6 +8,15 @@
  4. {% block javascripts %}{% endblock %}
  5. </head>
  6. <body>
  7. + <header>
  8. + <h1><a href="{{ path('homepage') }}">Guestbook</a></h1>
  9. + <ul>
  10. + {% for conference in conferences %}
  11. + <li><a href="{{ path('conference', { id: conference.id }) }}">{{ conference }}</a></li>
  12. + {% endfor %}
  13. + </ul>
  14. + <hr />
  15. + </header>
  16. {% block body %}{% endblock %}
  17. </body>
  18. </html>

把这段代码添加到布局里,这意味着所有继承布局的模板都要定义一个名为 conferences 的变量,这个变量由控制器创建并传递给模板。

由于我们只有 2 个控制器,你 可能 会要做下面的操作(不要照这样去改你的代码,我们很快会学习一种更好的方式):

  1. --- a/src/Controller/ConferenceController.php
  2. +++ b/src/Controller/ConferenceController.php
  3. @@ -29,12 +29,13 @@ class ConferenceController extends AbstractController
  4. }
  5. #[Route('/conference/{id}', name: 'conference')]
  6. - public function show(Request $request, Conference $conference, CommentRepository $commentRepository): Response
  7. + public function show(Request $request, Conference $conference, CommentRepository $commentRepository, ConferenceRepository $conferenceRepository): Response
  8. {
  9. $offset = max(0, $request->query->getInt('offset', 0));
  10. $paginator = $commentRepository->getCommentPaginator($conference, $offset);
  11. return new Response($this->twig->render('conference/show.html.twig', [
  12. + 'conferences' => $conferenceRepository->findAll(),
  13. 'conference' => $conference,
  14. 'comments' => $paginator,
  15. 'previous' => $offset - CommentRepository::PAGINATOR_PER_PAGE,

想象一下你必须更新几十个控制器,而且新的控制器里也要这样做。这不太现实。肯定有一个更好的方式。

Twig 支持全局变量。全局变量 在所有渲染的模板中可用。你可以在一个配置文件中定义它们,但这只适用于值不变的全局变量。若要添加一个 Twig 全局变量来引用所有会议,我们需要创建一个 监听器

认识 Symfony 的事件

Symfony 自带一个事件分发组件。分发器在某些特定的时刻 分发 某些 事件监听器 则监听这些事件。 监听器 就是接入框架内部的钩子。

例如,一些事件允许你和 HTTP 请求的生命周期进行交互。在处理一个请求的过程中,分发器会分发一些事件,这可以发生在请求对象被创建的时候,或是在控制器即将执行的时候,也可以在应答对象就绪并准备发送的时候,还可以在一个异常抛出的时候。监听器 可以监听一个或多个事件,并且根据事件上下文执行一些逻辑。

事件是一些定义良好的扩展点,它使得这个框架适应于更多的场景,也更具扩展性。很多 Symfony 组件,比如 Security 组件、Messenger 组件、Workflow 组件和 Mailer 组件大量使用事件。

另一个自带的事件和监听器的实际案例就是命令的生命周期:你可以创建一个监听器,让它在 任何 命令执行之前运行一些代码。

任何包和 bundle 可以分发自己的事件,这使得它们的代码更具扩展性。

为了避免写一个配置文件来描述监听器要监听哪些事件,可以创建一个 订阅器。一个订阅器也是一个监听器,但它有一个静态的 getSubscribedEvents() 方法,该方法返回配置信息。这个机制使得订阅器可以自动注册到 Symfony 的事件分发器。

实现一个订阅器

你对于使用 Maker Bundle 来生成代码应该牢记于心了,现在用它来生成一个订阅器:

  1. $ symfony console make:subscriber TwigEventSubscriber

这个命令会问你要监听哪个事件。选择``SymfonyComponentHttpKernelEventControllerEvent``事件,该事件在控制器被调用前那一刻被分发。这是注入``conferences``全局变量的最好时机,这样当控制器渲染模板时,Twig就可以使用它。按照下面的代码来更新你的订阅器:

patch_file

  1. --- a/src/EventSubscriber/TwigEventSubscriber.php
  2. +++ b/src/EventSubscriber/TwigEventSubscriber.php
  3. @@ -2,14 +2,25 @@
  4. namespace App\EventSubscriber;
  5. +use App\Repository\ConferenceRepository;
  6. use Symfony\Component\EventDispatcher\EventSubscriberInterface;
  7. use Symfony\Component\HttpKernel\Event\ControllerEvent;
  8. +use Twig\Environment;
  9. class TwigEventSubscriber implements EventSubscriberInterface
  10. {
  11. + private $twig;
  12. + private $conferenceRepository;
  13. +
  14. + public function __construct(Environment $twig, ConferenceRepository $conferenceRepository)
  15. + {
  16. + $this->twig = $twig;
  17. + $this->conferenceRepository = $conferenceRepository;
  18. + }
  19. +
  20. public function onControllerEvent(ControllerEvent $event)
  21. {
  22. - // ...
  23. + $this->twig->addGlobal('conferences', $this->conferenceRepository->findAll());
  24. }
  25. public static function getSubscribedEvents()

现在,你可以添加任意多的控制器:conferences 变量在 Twig 里总是可用。

注解

在以后的步骤中,我们会谈到一个性能好很多的替代方案。

按照年份和城市对会议进行排序

将会议按照年份排序可以让浏览变得更容易。我们可以创建一个定制方法来获取会议并对它们排序,但这里我们不用这个方式,而是去覆盖 findAll() 方法的默认实现,这样可以确保程序的所有地方都应用这个排序。

patch_file

  1. --- a/src/Repository/ConferenceRepository.php
  2. +++ b/src/Repository/ConferenceRepository.php
  3. @@ -19,6 +19,11 @@ class ConferenceRepository extends ServiceEntityRepository
  4. parent::__construct($registry, Conference::class);
  5. }
  6. + public function findAll()
  7. + {
  8. + return $this->findBy([], ['year' => 'ASC', 'city' => 'ASC']);
  9. + }
  10. +
  11. // /**
  12. // * @return Conference[] Returns an array of Conference objects
  13. // */

在本步骤的结尾,网站看上去应该像这样:

步骤 12: 监听事件 - 图1

深入学习


This work, including the code samples, is licensed under a Creative Commons BY-NC-SA 4.0 license.