Home > Back-end >  Symfony6 changing the controller manually using the "kernel.controller" event. How to inje
Symfony6 changing the controller manually using the "kernel.controller" event. How to inje

Time:05-05

The application that I am building is not going to work in a traditional way. All the routes ar going to be stored in the database. And based on the route provided I need to get the correct controller and action to be executed.

As I understand this can be achieved using the "kernel.controller" event listener: https://symfony.com/doc/current/reference/events.html#kernel-controller

I am trying to use the docs provided, but the example here does not exacly show how to set up a new callable controller to be passed. And I have a problem here, because I dont know how to inject the service container to my newly called controller.

At first the setup:

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 }

The listener:

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');
        }

    }
}

So this is the most basic example. I get the route from the database, if it a "homepage" route, I create the new HomepageController and set the action. However I am missing the container interface that I dont know how to inject. I get this error:

Call to a member function has() on null

on line: vendor\symfony\framework-bundle\Controller\AbstractController.php:216

which is:

/**
 * 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);
}

The controller is as basic as it gets:

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

So basically the container is not set. If I dump the $event->getController() I get this:

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

I need to set the container by doing $controller->setContainer(), but what do I pass?

CodePudding user response:

Do not inject the container, controllers are services too and manually instanciating them is preventing you from using constructor dependency injection. Use a service locator which contains only the controllers:

Declared in config/services.yaml:

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

Then in the event listener, add the service locator argument and fetch the fully configured controllers from it:


# ...
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;
            }
        }

        # ...
    }
}

If you dump any controller you will see that the container is set. Same will go for additionnal service that you autowire from the constructor.

  • Related