Last active
July 21, 2021 17:22
-
-
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.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?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