At the moment of writing this I’m testing the Symfony 4 Framework after reading the following article and also Fabian’s latest posts on Medium. I’m testing weather the not yet officially released version, will be a future candidate for some new project or not. But the interesting part of this research is that I already realized what I won’t do in new projects. But let’s focus first on the mission of describing how routing works internally in the framework.
So we will go from the Frontend Controller up to the internal guts that say from the route /test let’s execute Controller:test()
http:// bestpractices.local/test
The front-end controller located in /web/index.php bootstraps the framework:
$kernel = new Kernel(getenv('APP_ENV'), getenv('APP_DEBUG')); $request = Request::createFromGlobals(); $response = $kernel->handle($request); $response->send();
As we already know all this framework is based on Request -> Response concept from the beginning to the latest controller action and the Kernel is no exception. If we open this Kernel we can see that it’s mission is to setup Cache and Logs directories, register the bundles, configure the Container and Routing.
To go deeper in the chain we need to open HttpKernel with the mission described as:
HttpKernel notifies events to convert a Request object to a Response one.
There on the handleRaw method “Handles a request to convert it to a response” is where the whole framework workflow takes place:
namespace Symfony\Component\HttpKernel; class HttpKernel implements HttpKernelInterface, TerminableInterface { private function handleRaw(Request $request, $type = self::MASTER_REQUEST) { $this->requestStack->push($request); // request $event = new GetResponseEvent($this, $request, $type); $this->dispatcher->dispatch(KernelEvents::REQUEST, $event); // At this point the routing is already resolved // dump($request->attributes);exit(); if ($event->hasResponse()) { return $this->filterResponse($event->getResponse(), $request, $type); } // load controller if (false === $controller = $this->resolver->getController($request)) { throw new NotFoundHttpException(sprintf('Unable to find the controller for path "%s". The route is wrongly configured.', $request->getPathInfo())); } // ... Return a response
As we can see on the first lines, the Routing part, is resolved dispatching an Event called KernelEvents::REQUEST
namespace Symfony\Component\HttpKernel\EventListener; class RouterListener implements EventSubscriberInterface { public function onKernelRequest(GetResponseEvent $event) { $request = $event->getRequest(); $this->setCurrentRequest($request); if ($request->attributes->has('_controller')) { // routing is already done return; } // add attributes based on the request (routing) try { // matching a request is more powerful than matching a URL path + context, so try that first if ($this->matcher instanceof RequestMatcherInterface) { $parameters = $this->matcher->matchRequest($request); // dump($parameters); // here to see it matched } else { $parameters = $this->matcher->match($request->getPathInfo()); }
And in this part of the code we can see that dumping the $parameters the Request attributes are already assigned
$parameters = array( "_controller" => "App\Controller\DefaultController::testAction" "_route" => "test" );
Note: Request->attributes
is used to store information about the current Request such as the matched route, the controller, etc
But more interesting is to replace this dump for a : dump($this->matcher);
Then we can see that the real matcher of the routing is done by srcDevDebugProjectContainerUrlMatcher since we are currently using DEV as an environment.
This file, that we can find in this path /var/cache/dev/srcDevDebugProjectContainerUrlMatcher.php , is the responsible of returning the request parameters with the right controller to be executed for this route.
Since doing all the routing on the fly will be a real show stopper and we are concerned with performance: reading the configuration from the filesystem may slow down the application.
That’s why there’s a PhpMatcherDumper
class which can generate an implementation of UrlMatcherInterface
with all configuration in an optimized way.
Even in DEV, this UrlMatcher file, is only rendered again if we update any part of the routing. That’s one of the reasons clearing all cache will make a slow first request.
Lessons learnt from this research are:
- Keep your routes clean
Not used / deprecated routes that are still there will make this UrlMatcher unnecessary long and hence slower to resolve. - Avoid at all costs using Annotations for the routing
It’s generally considered a bad practice in OOP and contains much more Cons than Pros - Avoid using Assetic
This generates fully unnecessary routes for every assset in your HTML templates. New versions of Symfony will not come with Assetic so it’s better to get along without it.
I never care to check this routing to the core before and I highly recommend from time to time to check the internals of Symfony. You will learn a lot of the framework essentials and also will be more cautious of how to take the best of it. In my personal case, I learnt much more doing this small research taking your own conclusions, than reading some other developer top 5 list on framework optimization.
The code to reproduce this can be downloaded from public repository:
https://github.com/martinberlin/symfony-flex