A journey from Request to Response – HTTP Principle – HttpFoundation 



A journey from Request to Response – HTTP Principle – HttpFoundation 

0 0


symfony-request-to-response-slides


On Github Babacooll / symfony-request-to-response-slides

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 !

A journey from Request to Response 02 / 2015 Michaël Garrez