Drupal implements the Front Controller design pattern. All HTTP requests are directed to the index.php
file, and a response is generated and returned.
With the architectural decision to use Object-Oriented Programming and Symfony Components, the file has remained very short and concise. However, the request and response aspects of the process are given prominence. This is the index.php
without comments:
<?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);
DrupalKernel
extends the HttpKernel
to add some Drupal-specific functionality to the Kernel
. However, the HttpKernelInterface
has only one method — handle()
— which deals with the conversion of Request
to Response
. After the response has been sent, the Kernel
calls the terminate()
method, which performs some tasks at the end of the request.
We are now going to explore what happens in this method.
// core/lib/Drupal/Core/DrupalKernel.php
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);
}
}
It is possible that nothing happens, but in most cases, the terminate()
method of the HttpKernel
class will be called. It looks like this:
// vendor/symfony/http-kernel/HttpKernel.php
public function terminate(Request $request, Response $response) {
$this->dispatcher->dispatch(KernelEvents::TERMINATE, new PostResponseEvent($this, $request, $response));
}
This is part of the Symfony EventDispactcher Component. All that happens here is the KernelEvents::TERMINATE
event is dispatched. Nothing more, nothing less. However, other parts of the application that have subscribed to this event will be listening for it. When the broadcast gets to them, they act accordingly.
When event subscribers get registered, they may have a priority, and if none is specified, it defaults to 0. The registered methods will now be executed according to priority — a higher value means it is more important, and if two have the same, the one that is added to the dispatcher first gets executed first. There are 5 key listeners for this event.
-
core/modules/user/src/EventSubscriber/UserRequestSubscriber.php
class. (Priority: 300):In the
onKernelTerminate()
method of this class, the current user's last access time is updated. Usually, there will be lots of requests, so we ensure this update is done every 180 seconds or more, and this interval is configurable assession_write_interval
insettings.php
. -
core/lib/Drupal/Core/EventSubscriber/PathSubscriber
(Priority: 200):Another
onKernelTerminate()
method here makes sure we cache the system paths for the request. -
core/lib/Drupal/Core/EventSubscriber/RequestCloseSubscriber
(Priority: 100):This invokes the
onTerminate()
method which performs the cache writing as part of the request ending. -
core/modules/automated_cron/src/EventSubscriber/AutomatedCron
(Priority: 100):If automated cron is enabled, it is run in the
onTerminate()
method here. -
core/lib/Drupal/Core/EventSubscriber/KernelDestructionSubscriber
(Priority: -100):The
onKernelTerminate()
method here is designed to be called last as it is the lowest priority. If the service container was initialized during this request, this is where it is destroyed.
The concept of performing these tasks at the end of a request is not a new thing. In Drupal 7 this is what the drupal_page_footer()
function does. Here is the code:
// includes/common.inc
/** * // Performs end-of-request tasks. * * This function sets the page cache if appropriate, and allows modules to * react to the closing of the page by calling hook_exit(). */
function drupal_page_footer() {
global $user;
module_invoke_all('exit');
// Commit the user session, if needed.
drupal_session_commit();
if (variable_get('cache', 0) && ($cache = drupal_page_set_cache())) {
drupal_serve_page_from_cache($cache);
}
else {
ob_flush();
}
_registry_check_code(REGISTRY_WRITE_LOOKUP_CACHE);
drupal_cache_system_paths();
module_implements_write_cache();
drupal_file_scan_write_cache();
system_run_automated_cron();
}
It invokes hook_exit
on all modules, writes the user session, caches system paths, writes cache and runs automated cron. This is more or less what happens when the KernelEvents::TERMINATE
event is dispatched.
Conclusion
Drupal 8 may seem overly complex but, in reality, it is not hard to understand. In most cases, we are solving the same old problems in a different and possibly better-structured way. This is exemplified by the terminate()
method of the DrupalKernel
class, which is very similar to the old drupal_page_footer()
function in Drupal 7.
The knowledge gained by learning Drupal 8 can be of benefit in other applications built on similar Symfony components, for example Laravel and Sylius.