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.