Skip to content
Advertisement

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

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: AppEntityTranslation
    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'

    AppTranslationDbLoader:
        tags:
            - { name: translation.loader, alias: db }

    AppExtensionTwigExtension:
        arguments:
            - '@service_container'
        tags:
            - { name: twig.extension }

    AppEventListenerRequestListener:
        tags:
            - { name: kernel.event_listener, event: kernel.controller, method: onControllerRequest }

The listener:

RequestListener.php

<?php

namespace AppEventListener;

use AppControllerShopHomepageController;
use AppEntitySeoUrl;
use DoctrinePersistenceManagerRegistry;
use Exception;
use PsrContainerContainerInterface;
use SymfonyComponentDependencyInjectionParameterBagParameterBagInterface;
use SymfonyComponentHttpFoundationRequestStack;
use SymfonyComponentHttpKernelEventControllerEvent;
use SymfonyComponentRoutingRouterInterface;
use SymfonyComponentSecurityCoreAuthenticationTokenStorageTokenStorageInterface;
use SymfonyComponentSecurityCoreSecurity;

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: vendorsymfonyframework-bundleControllerAbstractController.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 AppControllerShop;

use AppRepositoryCategoryRepository;
use AppRepositoryShopProductRepository;
use SymfonyBundleFrameworkBundleControllerAbstractController;
use SymfonyComponentHttpFoundationRequest;
use SymfonyComponentHttpFoundationResponse;
use SymfonyComponentRoutingAnnotationRoute;

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 => AppControllerShopHomepageController {#417 ▼
    #container: null
  }
  1 => "index"
]

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

Advertisement

Answer

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:
    AppEventListenerRequestListener:
        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 AppControllerShopHomepageController;
use SymfonyComponentDependencyInjectionServiceLocator;

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.

User contributions licensed under: CC BY-SA
3 People found this is helpful
Advertisement