A journey from Request to Response


02 / 2015
Michaël Garrez
Why ? Isn't a framework supposed to prevent worrying about how it works ?
- Resolving edge cases
- Debugging made easily
- Improving general culture
- Learning about low level infrastructure of modern frameworks
- Avoiding blackbox situation
A journey from Request to Response

Yes ... but it helps :

Symfony Flow
A journey from Request to Response


HTTP Principle
A journey from Request to Response

HTTP protocol implies necessarily a Request and a Response.
How could a Web Framework be based on another principle ?
HTTP Principle
A journey from Request to Response


PHP Principle
A journey from Request to Response

Request
PHP gives us "helpers" to access HTTP Request data : Superglobals ($_GET, $_POST, $_COOKIE, $_FILES, $_SERVER).
Response
PHP methods like echo(), print(), cookies() and headers() gives you an easy way to output a HTTP Response.
Symfony Principle
A journey from Request to Response

HttpFoundation Symfony Component
One of Symfony's 31 components.
HttpFoundation provides an object oriented layer for a bunch of HTTP specifications.
HttpFoundation
A journey from Request to Response

Request
<?php
use Symfony\Component\HttpFoundation\Request;
$request = Request::createFromGlobals();
// OR
$request = new Request(
$_GET,
$_POST,
array(),
$_COOKIE,
$_FILES,
$_SERVER
);
HttpFoundation
A journey from Request to Response

Request
HttpFoundation will not sanitize your HTTP Request data, it only provides an object layer to access it

HttpFoundation
A journey from Request to Response

Response
<?php
use Symfony\Component\HttpFoundation\Response;
$response = new Response(
'Content',
Response::HTTP_OK,
array('content-type' => 'text/html')
);
HttpFoundation
A journey from Request to Response

Session
Provides an object layer to start or stop a session and access its data
HttpFoundation
A journey from Request to Response

Symfony Flow

HttpFoundation
A journey from Request to Response

Who uses it ?
- Drupal 8 (what we've been told ...)
- Laravel
- eZ Publish
- Silex
- Shopware
- ...
Front Controller
A journey from Request to Response

Symfony uses MVC architectural pattern.
Every single Request will be handle by the Front Controller to be dispatched to the Application
Front Controller
A journey from Request to Response

With the HttpFoundation Component you can actually build a correct Front Controller
<?php
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
$request = Request::createFromGlobals();
$path = $request->getPathInfo(); // Le chemin de l'URI demandée
if (in_array($path, array('', '/'))) {
$response = new Response('Bienvenue sur le site.');
} elseif ($path == '/contact') {
$response = new Response('Contactez nous');
} else {
$response = new Response('Page non trouvée.', 404);
}
$response->send();
Front Controller
A journey from Request to Response

Symfony Front Controllers (Depending on your environment)
uses a Kernel to handle things such as routing
<?php
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
$request = Request::createFromGlobals();
$path = $request->getPathInfo(); // Le chemin de l'URI demandée
if (in_array($path, array('', '/'))) {
$response = new Response('Bienvenue sur le site.');
} elseif ($path == '/contact') {
$response = new Response('Contactez nous');
} else {
$response = new Response('Page non trouvée.', 404);
}
$response->send();
Front Controller
A journey from Request to Response

Our Front Controller
<?php
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
$request = Request::createFromGlobals();
$path = $request->getPathInfo(); // Le chemin de l'URI demandée
if (in_array($path, array('', '/'))) {
$response = new Response('Bienvenue sur le site.');
} elseif ($path == '/contact') {
$response = new Response('Contactez nous');
} else {
$response = new Response('Page non trouvée.', 404);
}
$response->send();
<?php
use Symfony\Component\HttpFoundation\Request;
require_once __DIR__.'/../app/AppKernel.php';
$kernel = new AppKernel('prod', false);
$kernel->loadClassCache();
$request = Request::createFromGlobals();
$response = $kernel->handle($request);
$response->send();
$kernel->terminate($request, $response);
Symfony app.php
NB : boostrap.php.cache call has been removed from app.php for lisibility
Front Controller
A journey from Request to Response

Text

Symfony Flow
Symfony AppKernel
A journey from Request to Response

Globally the of Symfony :
- Registers bundles
- Boots bundles
- Initializes container
- Warms up cache
- Calls the HttpKernel
Symfony AppKernel
A journey from Request to Response

FrontController
<php
// Instantiates a new AppKernel in "prod" environment with no debug
$kernel = new AppKernel('prod', false);
// Will just define you want cache and it's suffix and extension (not a big deal :) !)
$kernel->loadClassCache();
Symfony AppKernel
A journey from Request to Response

FrontController
<?php
// Trivial HttpFoundation utilisation , isn't it ?
$request = Request::createFromGlobals();
// Real business is coming
$response = $kernel->handle($request);
Symfony AppKernel
A journey from Request to Response

handle($request)
- Boots your project
- Passes the Request to the HttpKernel
<?php
public function handle(Request $request, $type = HttpKernelInterface::MASTER_REQUEST, $catch = true)
{
if (false === $this->booted) {
$this->boot();
}
return $this->getHttpKernel()->handle($request, $type, $catch);
}
Symfony AppKernel
A journey from Request to Response

boot()
- Loads cache files mapped in classes.map (cache/ENV)
- Initializes bundles (basically creating an inheritance mapping)
-
Initializes a new Container (part of DependencyInjection Component) :
- From cache if is fresh
- Will inject itself (AppKernel) as 'kernel' into the Container
- The Container will register all parameters, services, ...
- Boots every bundle (basically does nothing !)
Symfony AppKernel
A journey from Request to Response


Symfony Flow
Symfony HttpKernel
A journey from Request to Response

- Implements same HttpKernelInterface as AppKernel
<?php
public function handle(Request $request, $type = HttpKernelInterface::MASTER_REQUEST, $catch = true)
{
if (false === $this->booted) {
$this->boot();
}
return $this->getHttpKernel()->handle($request, $type, $catch);
}
Symfony HttpKernel
A journey from Request to Response

handle($request)
Will convert our Request to a Response
Symfony HttpKernel
A journey from Request to Response

handle($request)
- Will call handleRaw($request)
Symfony HttpKernel
A journey from Request to Response

handleRaw($request)
<?php
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);
if ($event->hasResponse()) {
return $this->filterResponse($event->getResponse(), $request, $type);
}
// load controller
if (false === $controller = $this->resolver->getController($request)) {
throw new NotFoundHttpException('Unable to find the controller for path');
}
// ...
kernel.request
Symfony HttpKernel
A journey from Request to Response

ControllerResolver
The ControllerResolver will basically convert a :
DJUserBundle:Profile:show
To :
DJ\UserBundle\Controller\ProfileController::showAction
Based on the $request->attributes->get('_controller') value
Symfony HttpKernel
A journey from Request to Response

But how can the ControllerResolver access the attributes property of $request which is empty isn't it ?
ControllerResolver

Symfony HttpKernel
A journey from Request to Response

Yes remember :
ControllerResolver
<?php
$request = new Request(
$_GET,
$_POST,
array(),
$_COOKIE,
$_FILES,
$_SERVER
);
Symfony HttpKernel
A journey from Request to Response

Well, the Routing in Symfony is handled by a listener (RouterListener) on the kernel.request event dispatched before.
The RouterListener is a part of the Routing Component of Symfony
Before the RouterListener other listeners are triggered like SessionListener (part of Session Component), FragmentListener for sub requests
RouterListener
Symfony HttpKernel
A journey from Request to Response

The purpose of a MatcherInterface is to convert an URI (path exactly) to a route. In the fact the Router implements a RequestMatcherInterface which will call an UrlMatcherInterface child.
In the fact the Router will be rarely called but its cached version appENVUrlMatcher will be called. This file contains whole your application routes and its match() function only checks partial matches through hundred of strpos() calls on the $request->getPathInfo().
RouterListener
Symfony HttpKernel
A journey from Request to Response

Who uses the Routing ?
- Drupal 8
- phpBB 3.1
- eZ Publish
- Silex
- Shopware
- ...
Symfony HttpKernel
A journey from Request to Response

RouterListener
- Will listen inter alia to the kernel.request
-
Will call the Router (RequestMatcher) to match the Request to an existing route based on :
- Route requirements
- Route definition
- Will add the route informations to the attribute property of the request (_controller, _route, _route_params). _controller could be for example : DJProfileBundle:Profile:showAction
Symfony HttpKernel
A journey from Request to Response

RouterListener
<?php
public function onKernelRequest(GetResponseEvent $event)
{
$request = $event->getRequest();
// add attributes based on the request (routing)
try {
$parameters = $this->matcher->matchRequest($request);
$request->attributes->add($parameters);
unset($parameters['_route'], $parameters['_controller']);
$request->attributes->set('_route_params', $parameters);
} catch (ResourceNotFoundException $e) {
throw new NotFoundHttpException(...);
} catch (MethodNotAllowedException $e) {
throw new MethodNotAllowedHttpException(...);
}
}
Symfony HttpKernel
A journey from Request to Response

RouterListener
After the RouterListener, other Listeners will be called for the same event as the Firewall
Symfony HttpKernel
A journey from Request to Response

Symfony Flow

Symfony HttpKernel
A journey from Request to Response

Firewall
Symfony Firewall will not authenticate users.
Its only goal is to prevent users to access defined restricted areas

Symfony HttpKernel
A journey from Request to Response

Firewall
As said before the Firewall will listen to the kernel.request event.
It will try to match a secured area to the current path and will return listeners if match is found.
Symfony HttpKernel
A journey from Request to Response

Firewall
<?php
use Symfony\Component\Security\Http\FirewallMap;
use Symfony\Component\HttpFoundation\RequestMatcher;
use Symfony\Component\Security\Http\Firewall\ExceptionListener;
$map = new FirewallMap();
$requestMatcher = new RequestMatcher('^/secured-area/');
// instances de Symfony\Component\Security\Http\Firewall\ListenerInterface
$listeners = array(...);
$exceptionListener = new ExceptionListener(...);
$map->add($requestMatcher, $listeners, $exceptionListener);
// the EventDispatcher used by the HttpKernel
$dispatcher = ...;
$firewall = new Firewall($map, $dispatcher);
$dispatcher->addListener(KernelEvents::REQUEST, array($firewall, 'onKernelRequest');
Symfony HttpKernel
A journey from Request to Response

Authentication Listeners
Those listeners will try to Authenticate the user based on the request and will (depending on the situation) :
- Authenticate a User
- Throw an AuthenticationException (AccessDeniedHttpException)
- Do nothing
- Transfer to an AuthenticationEntryPointInterface (if not authenticated at all)
Symfony HttpKernel
A journey from Request to Response

handleRaw($request)
<?php
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);
if ($event->hasResponse()) {
return $this->filterResponse($event->getResponse(), $request, $type);
}
// load controller
if (false === $controller = $this->resolver->getController($request)) {
throw new NotFoundHttpException('Unable to find the controller for path');
}
// ...
Symfony HttpKernel
A journey from Request to Response

ResolverController
<?php
private function handleRaw(Request $request, $type = self::MASTER_REQUEST)
{
// ...
// load controller
if (false === $controller = $this->resolver->getController($request)) {
throw new NotFoundHttpException('Unable to find the controller for path');
}
// ...
The getController() method of the ResolverController will :
- Transforms DJProfileBundle:Profile:show to DJ\ProfileBundle\Controller\ProfileController::showAction
- Will instantiate the Controller (And sets the Container if ContainerAwareInterface is implemented)
Symfony HttpKernel
A journey from Request to Response

Symfony Flow

Symfony HttpKernel
A journey from Request to Response

handleRaw($request)
<?php
private function handleRaw(Request $request, $type = self::MASTER_REQUEST)
{
// ...
// load controller
if (false === $controller = $this->resolver->getController($request)) {
throw new NotFoundHttpException(...);
}
$event = new FilterControllerEvent($this, $controller, $request, $type);
$this->dispatcher->dispatch(KernelEvents::CONTROLLER, $event);
$controller = $event->getController();
// controller arguments
$arguments = $this->resolver->getArguments($request, $controller);
// call controller
$response = call_user_func_array($controller, $arguments);
// Your controller might not be returning a Response right away, this will be explained
// in the next slide don't worry :)
return $this->filterResponse($response, $request, $type);
}
Symfony HttpKernel
A journey from Request to Response

handleRaw($request)
<?php
private function handleRaw(Request $request, $type = self::MASTER_REQUEST)
{
// ...
if (!$response instanceof Response) {
$event = new GetResponseForControllerResultEvent($this, $request, $type, $response);
$this->dispatcher->dispatch(KernelEvents::VIEW, $event);
if ($event->hasResponse()) {
$response = $event->getResponse();
}
if (!$response instanceof Response) {
$msg = sprintf('The controller must return a response (%s given).', $this->varToString($response));
throw new \LogicException($msg);
}
}
return $this->filterResponse($response, $request, $type);
}
kernel.view
Symfony implements a kernel.view event to allow developers to implement a view subsystem.
Symfony HttpKernel
A journey from Request to Response

Symfony Flow

Symfony HttpKernel
A journey from Request to Response

filterResponse($response, $request)
Will pop the Request from the RequestStack by calling finishRequest()
Will return the Response to the AppKernel which will return it to the Front Controller
<?php
private function filterResponse(Response $response, Request $request, $type)
{
$event = new FilterResponseEvent($this, $request, $type, $response);
$this->dispatcher->dispatch(KernelEvents::RESPONSE, $event);
$this->finishRequest($request, $type);
return $event->getResponse();
}
kernel.reponse
kernel.finish_request
Symfony HttpKernel
A journey from Request to Response

Who uses it ?
- Drupal 8
- phpBB 3.1
- eZ Publish
- Silex
- Shopware
- ...
Front Controller
A journey from Request to Response

<?php
$kernel = new AppKernel('prod', false);
$kernel->loadClassCache();
$request = Request::createFromGlobals();
$response = $kernel->handle($request);
$response->send();
$kernel->terminate($request, $response);
Response
A journey from Request to Response

send()
Will add headers()
Will set cookies()
Will simply echo your Response content :
<?php
/**
* Sends content for the current web response.
*
* @return Response
*/
public function sendContent()
{
echo $this->content;
return $this;
}
AppKernel
A journey from Request to Response

terminate()
Will call terminate() from the HttpKernel
Will throw the last Event of our journey !
kernel.terminate
<?php
public function terminate(Request $request, Response $response)
{
$this->dispatcher->dispatch(KernelEvents::TERMINATE, new PostResponseEvent($this, $request, $response));
}
Errors ? Exceptions ?
A journey from Request to Response


Errors ? Exceptions ?
A journey from Request to Response

What if any part of my script triggers an exception ?
<?php
public function handle(Request $request, $type = HttpKernelInterface::MASTER_REQUEST, $catch = true)
{
try {
return $this->handleRaw($request, $type);
} catch (\Exception $e) {
if (false === $catch) {
$this->finishRequest($request, $type);
throw $e;
}
return $this->handleException($e, $request, $type);
}
}
kernel.exception
Errors ? Exceptions ?
A journey from Request to Response

Every \Exception thrown will be converted to a FlattenException by the ExceptionListener and passed to the designated Controller to handle them and return a Response
Sub Requests
A journey from Request to Response

Symfony allows you to launch sub Requests to render small parts of your website for example.

<?php
$kernel->handle($request, HttpKernelInterface::SUB_REQUEST);
A journey from Request to Response

A journey from Request to Response

Other Components seen in this presentation :
EventDispatcher
DependencyInjection
Finder
OptionsResolver
...
A journey from Request to Response

What about HttpCache ?
Symfony provides an HttpCache reverse proxy (which implements HttpKernelInterface like HttpKernel).
It takes an AppKernel as argument.
<?php
// ...
require_once __DIR__.'/../app/AppCache.php';
$kernel = new AppKernel('prod', false);
$kernel->loadClassCache();
$kernel = new AppCache($kernel);
Request::enableHttpMethodParameterOverride();
$request = Request::createFromGlobals();
$response = $kernel->handle($request);
$response->send();
$kernel->terminate($request, $response);
A journey from Request to Response

What about Command ?
When you trigger php app/console ... you actually launch an Application.
The Symfony applications also wraps the AppKernel to get access to the bundles, Container, ...
<?php
// ...
$input = new ArgvInput();
$kernel = new AppKernel($env, $debug);
$application = new Application($kernel);
$application->run($input);
Thank you !
Symfony2
By Babacooll
Symfony2
- 979