Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Symfony - Changing how controllers are instantiated and executed

Tags:

php

symfony

Note: as of version 2.8, Symfony provided autowire: true for service configuration, and as of version 3.3, Symfony provided alias (instead of autowire_types) to alias a concrete object to an interface for automatic dependency injection into 'controllers as services'. There's also a bundle to allow autowiring for controller 'action' methods, although I've moved away from this and have focussed more on a variation of the ADR pattern (which is, basically, a single 'action' class with an interface method and not shoving a load of actions methods within a single class which eventually makes for an architectural nightmare). This is, effectively, what I've been looking for all these years and now no longer need to 'hook-in' a decent recursive dependency injector (auryn) as the framework now handles what it should have four years previous. I'll leave this answer here in case anyone wants to trace the steps that I did to see how the kernel works and some of the options available at this level.


Note: Although this question primarily targets Symfony 3, it should also be relevant to users of Symfony 2 as the kernel logic doesn't seem to have changed much.

I want to change how controllers are instantiated in Symfony. The logic for their instantiation currently resides in HttpKernel::handle and, more specifically, HttpKernel::handleRaw. I want to replace call_user_func_array($controller, $arguments) with my own injector performing that specific line instead.

The options I have tried thus far:

  • Extending HttpKernel::handle with my own method and then having this called by symfony
http_kernel:
    class: AppBundle\HttpKernel
    arguments: ['@event_dispatcher', '@controller_resolver', '@request_stack']

The downside of this is that, because handleRaw is private, I can't extend it without hacky reflection and so I would have to copy and paste a tonne of code.

  • Creating and registering a new controller resolver
controller_resolver:
    class: AppBundle\ControllerResolver
    arguments: []

This was a fundamental misunderstanding I had so I thought I'd document it here. The resolver's job is to resolve where to find the controller as a callable. It hasn't actually been called yet. I am more than happy with how Symfony takes the routes from routes.yml and figures out the class and method to call for the controller as a callable.

  • Adding an event listener on kernel.request
kernel.request:
    class: MyCustomRequestListener
    tags:
        - { name: kernel.event_listener, event: kernel.request, method: onKernelRequest, priority: 33 /** Important, we'll get to why in a minute **/ }

Taking a look at the Http Kernel Component Documentation, we can see that it has the following typical purpose:

To add more information to the Request, initialise parts of the system, or return a Response if possible (e.g. a security layer that denies access).

I figured that by creating a new listener, using my custom injector to create my controller, and then return a response in that listener, would bypass the rest of the code that instantiates the controller. This is what I want! But there's a major flaw with this:

The Symfony Profiler doesn't show up or any of that stuff, it's just my response and that's it. Dead. I found that I can switch the priority from 31 to 33 and have it switch between my code and Symfonys, and I believe this is because of the router listener priority. I feel I'm going down the wrong path here.

  • Listening on the kernel.controller event.

No, this allows me to change the callable that will be called by call_user_func_array(), not how the controller is actually instantiated, which is my goal.

I've documented my ideas but I'm out. How can I achieve the following?

  • Change how the controllers are instantiated and then executed, specifically call_user_func_array() which is in a bloody private method (thanks Symfony)
  • Fall back to the default controller instantiation if mine doesn't work
  • Allow everything else to work as expected, like the profiler loading
  • Be able to bundle this up with an extension for other users

Why do I want to do this?

Controllers can have many different methods for different circumstances and each method should be able to typehint for what it individually requires rather than having a constructor take all the things, some of which may not even be used depending on the controller method being executed. Controllers don't really adhere to the Single Responsibility Principle, and they're an 'object edge case'. But they are what they are.

I want to replace how controllers are created with my own recursively autowiring injector, and also how they are executed, again with recursive introspection via my injector, as the default Symfony package does not seem to have this functionality. Even with the latest "autowire" service option in Symfony 2.8+.

like image 907
Jimbo Avatar asked Jan 19 '16 11:01

Jimbo


1 Answers

The controller resolver actually does two things. The first is to get the controller. The second is to get a list of arguments for a given action.

$arguments = $this->resolver->getArguments($request, $controller);
$response = call_user_func_array($controller, $arguments);

It is the getArguments method that you could override to implement your special "action method injection" functionality. You just need to determine what arguments the action method needs and return an array of them.

Based on a different question, I also think you might be misunderstanding the autowire functionality. Autowire really only applies to constructor injection. It's not going to help with action method injection.

If the getArguments does not solve your requirement then overriding the handle method is really your only option. Yes there is quite a bit of code to copy/paste from handleRaw but that is because there is quite a bit to do in there. And even if handleRaw was protected you would still have to copy/paste the code just to get at the one line you want to replace.

like image 51
Cerad Avatar answered Oct 26 '22 11:10

Cerad