Symfony6 使用 "kernel.controller" 事件手动更改控制器。如何注入服务容器?

Symfony6 changing the controller manually using the "kernel.controller" event. How to inject the service container?

我正在构建的应用程序无法以传统方式运行。所有路线都将存储在数据库中。根据提供的路线,我需要获得正确的控制器和要执行的操作。

据我所知,这可以使用“kernel.controller”事件侦听器来实现:https://symfony.com/doc/current/reference/events.html#kernel-controller

我正在尝试使用提供的文档,但此处的示例并未准确显示如何设置要传递的新可调用控制器。我在这里遇到了一个问题,因为我不知道如何将服务容器注入到我新调用的控制器中。

首先设置:

services.yaml

parameters:
    db_i18n.entity: App\Entity\Translation
    developer: '%env(DEVELOPER)%'
    category_directory: '%kernel.project_dir%/public/uploads/category'
    temp_directory: '%kernel.project_dir%/public/uploads/temp'
    product_directory: '%kernel.project_dir%/public/uploads/product'
    app.supported_locales: 'lt|en|ru'
services:
    _defaults:
        autowire: true
        autoconfigure: true

    App\:
        resource: '../src/'
        exclude:
            - '../src/DependencyInjection/'
            - '../src/Entity/'
            - '../src/Kernel.php'

    App\Translation\DbLoader:
        tags:
            - { name: translation.loader, alias: db }

    App\Extension\TwigExtension:
        arguments:
            - '@service_container'
        tags:
            - { name: twig.extension }

    App\EventListener\RequestListener:
        tags:
            - { name: kernel.event_listener, event: kernel.controller, method: onControllerRequest }

听众:

RequestListener.php

<?php

namespace App\EventListener;

use App\Controller\Shop\HomepageController;
use App\Entity\SeoUrl;
use Doctrine\Persistence\ManagerRegistry;
use Exception;
use Psr\Container\ContainerInterface;
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\HttpKernel\Event\ControllerEvent;
use Symfony\Component\Routing\RouterInterface;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
use Symfony\Component\Security\Core\Security;

class RequestListener
{
    public ManagerRegistry $doctrine;
    public RequestStack $requestStack;

    public function __construct(ManagerRegistry $doctrine, RequestStack $requestStack)
    {
        $this->doctrine = $doctrine;
        $this->requestStack = $requestStack;
    }

    /**
     * @throws Exception
     */
    public function onControllerRequest(ControllerEvent $event)
    {
        if (!$event->isMainRequest()) {
            return;
        }

        if(str_contains($this->requestStack->getMainRequest()->getPathInfo(), '/admin')) {
            return;
        }

        $em = $this->doctrine->getManager();
        $pathInfo = $this->requestStack->getMainRequest()->getPathInfo();
;
        $route = $em->getRepository(SeoUrl::class)->findOneBy(['keyword' => $pathInfo]);

        if($route instanceof SeoUrl) {
            switch ($route->getController()) {
                case 'homepage':
                    $controller = new HomepageController();
                    $event->setController([$controller, $route->getAction()]);
                    break;
                default:
                    break;
            }
        } else {
            throw new Exception('Route not found');
        }

    }
}

所以这是最基本的例子。我从数据库获取路由,如果它是“主页”路由,我创建新的 HomepageController 并设置操作。但是我缺少我不知道如何注入的容器接口。我收到此错误:

调用成员函数 has() on null

在线:vendor\symfony\framework-bundle\Controller\AbstractController.php:216

即:

/**
 * Returns a rendered view.
 */
protected function renderView(string $view, array $parameters = []): string
{
    if (!$this->container->has('twig')) { // here
        throw new \LogicException('You cannot use the "renderView" method if the Twig Bundle is not available. Try running "composer require symfony/twig-bundle".');
    }

    return $this->container->get('twig')->render($view, $parameters);
}

控制器是最基本的:

HomepageController.php

<?php

namespace App\Controller\Shop;

use App\Repository\CategoryRepository;
use App\Repository\Shop\ProductRepository;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;

class HomepageController extends AbstractController
{
    #[Route('/', name: 'index', methods: ['GET'])]
    public function index(): Response
    {
        return $this->render('shop/index.html.twig', [
        ]);
    }
}

所以基本上容器没有设置。如果我转储 $event->getController() 我得到这个:

RequestListener.php on line 58:
array:2 [▼
  0 => App\Controller\Shop\HomepageController {#417 ▼
    #container: null
  }
  1 => "index"
]

我需要通过 $controller->setContainer() 来设置容器,但是我要传递什么?

不要注入容器,控制器也是服务,手动实例化它们会阻止您使用构造函数依赖注入。使用仅包含控制器的服务定位器:

config/services.yaml中声明:

# config/services.yaml
services:
    App\EventListener\RequestListener:
        arguments:
            $serviceLocator: !tagged_locator { tag: 'controller.service_arguments' }

然后在事件侦听器中,添加服务定位器参数并从中获取完全配置的控制器:


# ...
use App\Controller\Shop\HomepageController;
use Symfony\Component\DependencyInjection\ServiceLocator;

class RequestListener
{
    # ...
    private ServiceLocator $serviceLocator;

    public function __construct(
        # ...
        ServiceLocator $serviceLocator
    ) {
        # ...
        $this->serviceLocator = $serviceLocator;
    }

    public function onControllerRequest(ControllerEvent $event)
    {
        # ...

        if($route instanceof SeoUrl) {
            switch ($route->getController()) {
                case 'homepage':
                    $controller = $this->serviceLocator->get(HomepageController::class);
                    # ...
                    break;
                default:
                    break;
            }
        }

        # ...
    }
}

如果转储任何控制器,您将看到容器已设置。同样适用于您从构造函数自动装配的附加服务。