Skip to content

Instantly share code, notes, and snippets.

@jasonhofer
Last active July 21, 2021 17:22
Show Gist options
  • Save jasonhofer/0e47111b7ed1749487b5ceca6665e8fc to your computer and use it in GitHub Desktop.
Save jasonhofer/0e47111b7ed1749487b5ceca6665e8fc to your computer and use it in GitHub Desktop.
A simplified version of the Symfony HttpKernel to help show what it does.
<?php
use Psr\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\HttpFoundation as Http;
/**
* HttpKernel: "Get the response, and get out."
*
* As soon as the kernel gets its hands on a Response object, it says "I'm done" and returns it.
* It is only interested in finding a Response object, at which point it will call it a day.
*
* There are four events that this simplified kernel will trigger sequentially:
* - `kernel.request` -- Triggered immediately upon receiving the Request object.
* - `kernel.controller` -- Triggered after an initial attempt is made to resolve which controller will handle the request.
* - `kernel.view` -- Only triggered if the controller returns something other than a Response object.
* - `kernel.response` -- Triggered just before returning the Response object.
*
* There is a fifth event, `kernel.exception`, which is triggered when an exception is caught.
*
* Note: Normally some kind of dispatcher object would be created, loaded up with event handlers,
* and then passed to the kernel's contructor. In order to save space, I've left that out.
*
* The front controller, `index.php` for example, would contain the following:
*
* // ...assume $dispatcher has already been created and loaded with event handlers.
* $request = Http\Request::createFromGlobals();
* $kernel = new SimpleHttpKernel($dispatcher);
* $response = $kernel->handle($request);
* $response->send();
*
* @see https://github.com/symfony/http-kernel/blob/master/HttpKernel.php
* @see https://symfony.com/doc/current/components/http_kernel.html
*/
class SimpleHttpKernel
{
/** @var EventDispatcherInterface */
protected $dispatcher;
public function __construct(EventDispatcherInterface $dispatcher)
{
$this->dispatcher = $dispatcher;
}
/**
* @param Http\Request $request
* @param bool $catch
*
* @return Http\Response
*
* @throws \Exception
*/
public function handle(Http\Request $request, $catch = true)
{
try {
return $this->handleRequest($request);
} catch (\Exception $e) {
// We can bypass the kernel's exception handling by setting $catch to false.
if (false === $catch) {
throw $e;
}
return $this->handleException($e, $request);
}
}
protected function handleRequest(Http\Request $request)
{
// Give "kernel.request" event handlers first crack at the request.
$event = new KernelEvent($this, 'kernel.request', $request);
$this->dispatcher->dispatch($event);
// If any of the "kernel.request" event handlers have set a response object on the event, we're done!
if ($event->response instanceof Http\Response) {
return $event->response;
}
// Use the request object to determine which controller to call.
$controller = $this->resolveController($request);
// Give the "kernel.controller" event handlers a crack at whatever was resolved.
$event = new KernelEvent($this, 'kernel.controller', $request, $controller);
$this->dispatcher->dispatch($event);
$controller = $event->controller;
if (!is_callable($controller)) {
// If we don't have a callable controller at this point, there's nothing else that can be done.
throw new \LogicException('Failed to resolve controller to a callable.');
}
// Use the controller and the request object to determine which arguments to
// pass to the controller when calling it.
$arguments = $this->resolveControllerArguments($controller, $request);
// This is where the rubber meets the road.
$response = call_user_func_array($controller, $arguments);
// First chance!
if (!$response instanceof Http\Response) {
// If the controller did *not* return a Response object, then we let the "kernel.view"
// event handlers have a crack at converting whatever was returned into a Response object.
$event = new KernelEvent($this, 'kernel.view', $request, $response);
$this->dispatcher->dispatch($event);
$response = $event->response;
// Last chance!
if (!$response instanceof Http\Response) {
// If we do not have a Response object by this point, there's nothing else that can be done.
throw new \LogicException('Failed to resolve controller result to a Response object.');
}
}
return $this->filterResponse($response, $request);
}
protected function handleException(\Exception $e, Http\Request $request)
{
// Give the "kernel.exception" event handlers a chance to create an appropriate Response object.
$event = new KernelEvent($this, 'kernel.exception', $request, $e);
$this->dispatcher->dispatch($event);
if (!$event->response instanceof Http\Response) {
// If no Response object was set by a "kernel.exception" event handler, there's nothing else that can be done.
throw $event->exception;
}
try {
return $this->filterResponse($event->response, $request);
} catch (\Exception $e) {
return $event->response;
}
}
protected function resolveController(Http\Request $request)
{
// For simplicity we are assuming that, during the "kernel.request" event, an
// event handler *might* have set the "_controller" attribute on the request object.
return $request->attributes->get('_controller');
}
protected function resolveControllerArguments($controller, Http\Request $request)
{
// For simplicity we are only passing the request object itself to the controller.
// The controller can then resolve its own arguments.
return array($request);
}
/**
* Trigger the "kernel.response" event. This method only exists to prevent duplicating code between
* handleRequest() and handleException().
* @param Http\Response $response
* @param Http\Request $request
* @return Http\Response
*/
protected function filterResponse(Http\Response $response, Http\Request $request)
{
$event = new KernelEvent($this, 'kernel.response', $request, $response);
$this->dispatcher->dispatch($event);
return $event->response;
}
}
class KernelEvent
{
public $name;
public $kernel;
public $request;
public $response;
public $controller;
public $exception;
public function __construct(SimpleHttpKernel $kernel, $name, Http\Request $request, $extra = null)
{
$this->kernel = $kernel;
$this->name = $name;
$this->request = $request;
switch ($name) {
case 'kernel.controller': $this->controller = $extra; break;
case 'kernel.view': // fall-through
case 'kernel.response': $this->response = $extra; break;
case 'kernel.exception': $this->exception = $extra; break;
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment