Usually, web applications do one thing. A browser makes a request for a resource and the server replies with a response. It is Request in, Response out.
With a centralized request-handling mechanism, all requests pass through a single handler which kicks off a workflow doing things like authentication, authorization, or logging, until the resource is located and returned to the request agent, usually a web browser. This is the front controller design pattern.
In Drupal, all requests go through index.php
, which acts as the front controller. This file is very small, 13 lines long (without spaces, comments and opening PHP tag, 8 lines):
<?php
use Drupal\Core\DrupalKernel;
use Symfony\Component\HttpFoundation\Request;
$autoloader = require_once 'autoload.php';
$kernel = new DrupalKernel('prod', $autoloader);
$request = Request::createFromGlobals();
$response = $kernel->handle($request);$response->send();
$kernel->terminate($request, $response);
This does not look like any previous Drupal code. It is the Object-Oriented Programming (OOP) paradigm in practice. Let us go over it:
This first line summarizes a shift in the design principles adopted for Drupal 8. DrupalKernel
is a fusion of Drupal and Kernel, which comes from the Symfony framework. This Drupal version has its foundation in key components from Symfony.
This statement describes the namespace of the DrupalKernel
class. Namespaces allow unambiguous references to objects, functions or variables.
The main component is HttpFoundation, an OOP abstraction of the HTTP specification. PHP has many global variables representing a single request. HTTP being a stateless protocol means that, in order to deliver a simple web page, there may be hundreds of HTTP requests, with each one managing all the associated global variables. The Symfony Request object sanitizes an incoming request and encapsulates it in a single PHP object representing the HTTP request message.
In a large application with lots of files, the code needs to be made available when necessary. When we include or require a file in another, we instruct the PHP parser to replace the inclusion statement with the contents of the included file.
The require_once
statement in index.php
instructs the PHP parser to substiute the contents of autoload.php
where the statement is found. If we open up autoload.php
, we find a require
statement which references another autoload.php
file in the vendor
directory.
return require __DIR__ . '/../vendor/autoload.php';
This autoload.php
in turn requires an autoload file autoload_real.php
generated by Composer with a getLoader()
method, which returns an instance of the \Composer\Autoload\ClassLoader
class. This is what gets passed to index.php
as $autoloader
.
<?php
// autoload.php @generated by Composer
require_once __DIR__ . '/composer/autoload_real.php';
return ComposerAutoloaderInit719e7215c1bd9084c4a664aa14d9da18::getLoader();
All classes that conform to PSR-0 and PSR-4 class loading standards will be automatically included when required. PSR-0 and PSR-4 represent conventions agreed amongst members of the PHP Framework Interoperability Group (PHP-FIG) on folder structure and class file names. As long as the project adheres to the recommended structure, the file will be found and included.
As we have noted earlier, Drupal 8 may be considered a hybrid of Symfony and Drupal, as the name DrupalKernel
suggests. Following the
core/lib/Drupal/Core/DrupalKernel.php
file, which implements DrupalKernelInterface
in core/lib/Drupal/Core/DrupalKernelInterface.php
, one of the 2 interfaces it extends is HttpKernelInterface
from vendor/symfony/http-kernel/HttpKernelInterface.php
. This interface defines only one method:
public function handle(Request $request, $type = self::MASTER_REQUEST, $catch = true);
It receives a Request and returns a Response. Ultimately, that is what DrupalKernel
does. The HttpFoundation
component provides a structured and flexible process for converting a Request into a Response. This component is redundant without a Request
object. The handle()
method takes 3 parameters and apart from the Request
object, the other 2 are optional.
The name of the method says it all: createFromGlobals()
. An instance of Request object is created from PHP global variables. Also called "superglobals", these are variables created and managed by PHP and they are accessible everywhere in a PHP program. These are $GLOBALS, $_SERVER, $_GET, $_POST, $_FILES, $_COOKIE, $_SESSION, $_REQUEST and $_ENV. Potentially, they could be modified by a user and therefore cannot be trusted.
$_REQUEST holds any global data that is registered with the request_order
directive in the php.ini configuration file. This consists of the first letter of the variable names e.g. EGPCS
. If left empty, PHP will use the value of variables_order
which is similar to request_order
.
$GLOBALS holds all the variables in the global scope of the current script. $_SESSION contains all session variables available to the current script. $_FILES is an array of items uploaded to the current script. There are other factors affecting what is available as global variables to a script.
The HttpFoundation
component steps in with a Request object which sanitizes the incoming request with all its unpredictable global variables and encapsulates them in a single object representing the HTTP request message. It also provides in-built methods for accessing its data.
This is the ultimate task for a web application. It receives a request, understands it, and deals with it. The handle()
method differentiates Drupal from other applications built on HttpFoundation. Whilst there may be differences in detail, the principles remain the same.
// core/lib/Drupal/Core/DrupalKernel.php
public function handle(Request $request, $type = self::MASTER_REQUEST, $catch = TRUE) {
// Ensure sane PHP environment variables.
static::bootEnvironment();
try {
$this->initializeSettings($request);
// Redirect the user to the installation script if Drupal has not been
// installed yet (i.e., if no $databases array has been defined in the
// settings.php file) and we are not already installing.
if (!Database::getConnectionInfo() && !drupal_installation_attempted() && PHP_SAPI !== 'cli') {
$response = new RedirectResponse($request->getBasePath() . '/core/install.php', 302, ['Cache-Control' => 'no-cache']);
}
else {
$this->boot();
$response = $this->getHttpKernel()->handle($request, $type, $catch);
}
} catch (\Exception $e) {
if ($catch === FALSE) {
throw $e;
}
$response = $this->handleException($e, $request, $type);
}
// Adapt response headers to the current request. $response->prepare($request);
return $response;
}
Apart from taking adequate care of exceptions, there are 3 main methods:
bootEnvironment()
:
Drupal requires some PHP settings for it to work correctly or consistently.
initializeSettings()
:
The site path is located and the Drupal Settings
class is iniitialized. In addition, there is a check for the availability of an optimized class loader provided by the following extensions — APC, WinCache, and XCache which is then registered. This ensures the availability of classes in our application when required.
boot()
:
This is where the Service Container is built. There are many classes and objects in our application, and it is difficult to manage and keep track of them. The container is a special object that has other objects (described as services) in your application. The Dependency Injection component provides a centralized and standardized location, the Service Container, for an optimized initialization of objects.
When all these steps have been taken, the handle()
method of the HttpKernel
implementation is called. Inside that method, there is a call to handleRaw()
, where the conversion of Request to Response happens:
// vendor/symfony/http-kernel/HttpKernel.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(sprintf('Unable to find the controller for path "%s". The route is wrongly configured.', $request->getPathInfo()));
}
$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);
// view
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));
// the user may have forgotten to return something
if (null === $response) {
$msg .= ' Did you forget to add a return statement somewhere in your controller?';
}
throw new \LogicException($msg);
}
}
return $this->filterResponse($response, $request, $type);
}
All the objects in the application are initialized and managed centrally by the Dependency Injection component. The EventDispatcher component co-ordinates the management of different events. Registered events are fired, the system is notified, and objects that subscribe to them respond accordingly.
The request is analyzed and the Routing component matches the URL to a controller which eventually returns a response.
The handle()
method of a Request object always returns a Response object which is made up of a head (header) and body (content). The type of content needs to be specified in the header with "Content-Type:". This can be any MIME type. The name Multipurpose Internet Mail Extensions originated from the early days of the internet when these types were created for emails sent with the SMTP protocol. These days, they are better known as Internet Media Type.
public function send(){
$this->sendHeaders();
$this->sendContent();
if (function_exists('fastcgi_finish_request')) {
fastcgi_finish_request();
} elseif ('cli' !== PHP_SAPI) {
static::closeOutputBuffers(0, true);
}
return $this;
}
Using the "Content-type" header value defined in the sendHeaders()
method (called from send()
), the browser, or any application that makes the request for a resource, can open the file with the proper extension or plugin. This may be HTML, JSON, XML, plain text, image, audio, video, or any other acceptable format.
Perform any defined clean-up tasks in terminate()
:
public function terminate(Request $request, Response $response) {
// Only run terminate() when essential services have been set up properly
// by preHandle() before.
if (FALSE === $this->prepared) {
return;
}
if ($this->getHttpKernel() instanceof TerminableInterface) {
$this->getHttpKernel()->terminate($request, $response);
}
}
In most cases, nothing is required here, as $prepared
is FALSE
by default. However, there is a chance to carry out any desired housekeeping operations after a response has been sent to the request agent.
By studying the index.php
file, we have followed the journey of an HTTP Request into the Kernel and arrived at an HTTP Response. The complexity has been crystallized into small consistent steps that are similar in other applications built on Symfony components. Laravel, Sylius, the full Symfony stack, and, to a certain extent, ezPublish, have a front controller that can be mistaken for our index.php
. A good grasp of the internals here is a good platform for moving on to those frameworks and applications.