Upgrade Your Drupal Skills
We trained 1,000+ Drupal Developers over the last decade.
See Advanced Courses NAH, I know EnoughWe've made it to the final blog post (for now?) in our spotlight on Symfony in Drupal.
Spotlight on Symfony in Drupal
Utility Symfony components used in Drupal
Let's look at the other components.
We're using the term, "utility components", to group together Symfony components that provide useful functionality. It's a great idea to familiarize yourself with these. You can make use of them in your own modules, or bring them into other PHP application code.
Console
For Drupal developers, there are at least 2 readily-available options for building custom command-line utilities or applications: Symfony Console and Drush.
Symfony's Console component is useful for building command-line utilities for PHP applications. Remember the Drupal Console project? While no longer maintained, it used Console under-the-hood.
Drush has several points of extension including creating custom Drush commands. Drush includes the Drupal Code Generator project, a code scaffolding tool which you can extend to create custom commands.
If you're interested in adding Console functionality in Drupal core, check out this issue: Provide core CLI commands for the most common features of Drush.
Yaml
The Yaml component loads and parses YAML files. Drupal's configuration management system uses this one a lot.
Process
The Process component runs commands in a sub-process. It can handle system commands like exec
, passthru
, system
, or git
, and handles differences in operating systems for us.
Mime
The Mime component provides useful methods for working with MIME types and sending emails.
Polyfill
Symfony backports features from the latest PHP versions into polyfill components. These are automatically included depending on your configuration and PHP version.
Translation
Symfony's translation tools support the internationalization of an application.
Serializer
The Serializer component handles the encoding and decoding of objects among formats like XML, JSON, and Yaml.
Validator
The Validator component provides more of a design pattern, or an API of sorts that other modules can use to define what types of data they work with. These rules are defined as Constraints, and the code that actually evaluates the logic are called Validators.
Learn more
Explore Symfony in Drupal for yourself:
- Look in vendor/symfony for Symfony components.
- Browse core/lib/Drupal/Core and core/lib/Drupal/Component for classes that are original to Drupal. This is where you'll find Drupal's customizations of Symfony components.
Recap
We've taken a high-level look at key Symfony components in Drupal. If this article has sparked your curiosity, dig into the Symfony components documentation. Learn how to extend and alter Drupal. Looking under the hood of both Symfony and Drupal improves our understanding and makes us better developers. And these skills can transfer to other PHP projects that use Symfony.
In this installment of our series on Symfony's role in Drupal, we're focusing on the Routing component. Even if it may seem simple looking from the outside, routing in Drupal is a complex task with lots of customized parts. The routing component's job is to match incoming requests to the correct controller, which is then responsible for building the response.
If you want to get into the details of routing in Drupal, check out our in-depth course on Routes and Controllers in Drupal. But for now, let's take a high-level glance at how Drupal has built upon Symfony's Routing component.
Spotlight on Symfony in Drupal
- Part 1: HttpKernel in Drupal
- Part 2: EventDispatcher in Drupal
- Part 3: Routing in Drupal
- Part 4: Utility Components in Drupal
Symfony's routing component in Drupal
Find the heart of Symfony's routing component in Drupal in /vendor/symfony/routing/Router.php. There's a lot going on in this file, but we're going to take a look at its 2 primary methods: getRouteCollection
and match
.
/**
* @return RouteCollection
*/
public function getRouteCollection()
{
return $this->collection ??= $this->loader->load($this->resource, $this->options['resource_type']);
}
The getRouteCollection
method loads all the route mappings defined by the application. As an example, Drupal might define routes like this:
system.admin:
path: '/admin'
defaults:
_controller: '\Drupal\system\Controller\SystemController::systemAdminMenuBlockPage'
_title: 'Administration'
As you might imagine, loading all these routes is computationally expensive, so there is code here to help us cache and optimize this information.
Now let's look at the match
method.
public function match(string $pathinfo): array
{
return $this->getMatcher()->match($pathinfo);
}
The match
method identifies which route in our collection corresponds to the current request's path. It's instrumental in understanding which part of our code will respond to a particular request. You can see an example matcher implementation in the /vendor/symfony/routing/Matcher/UrlMatcher.php file. The UrlMatcher
class is the main matcher for translating between the path of an incoming request and the route that will ultimately generate a response.
How Drupal enhances Symfony's routing component
You can find Drupal's version of the Router in the core/lib/Drupal/Core/Routing directory. There's a lot more going on here than in the base Symfony routing component, all because Drupal adds several enhancements to meet its needs.
One of the most useful additions Drupal provides is access control at the route level. Have a look at AccessAwareRouter
class's implementation of the matchRequest
method:
public function matchRequest(Request $request): array {
$parameters = $this->router->matchRequest($request);
$request->attributes->add($parameters);
$this->checkAccess($request);
// We can not return $parameters because the access check can change the
// request attributes.
return $request->attributes->all();
}
Here, Drupal matches a URL to a controller and checks that the route has the correct access permissions, giving developers a helping hand in managing complex permission requirements. Our Add Access Checking to a Route tutorial can give you an in-depth understanding of this.
Next, you might be curious about how Drupal gathers information about routes. Delve into this by looking at the RouteBuilder.php file. It's packed with information on how Drupal builds its unique routing system.
// ...
public function rebuild() {
// ...
$collection = new RouteCollection();
foreach ($this->getRouteDefinitions() as $routes) {
// The top-level 'routes_callback' is a list of methods in controller
// syntax, see \Drupal\Core\Controller\ControllerResolver. These methods
// should return a set of \Symfony\Component\Routing\Route objects, either
// in an associative array keyed by the route name, which will be iterated
// over and added to the collection for this provider, or as a new
// \Symfony\Component\Routing\RouteCollection object, which will be added
// to the collection.
if (isset($routes['route_callbacks'])) {
foreach ($routes['route_callbacks'] as $route_callback) {
$callback = $this->controllerResolver->getControllerFromDefinition($route_callback);
if ($callback_routes = call_user_func($callback)) {
// If a RouteCollection is returned, add the whole collection.
if ($callback_routes instanceof RouteCollection) {
$collection->addCollection($callback_routes);
}
// Otherwise, add each Route object individually.
else {
foreach ($callback_routes as $name => $callback_route) {
$collection->add($name, $callback_route);
}
}
}
}
unset($routes['route_callbacks']);
}
foreach ($routes as $name => $route_info) {
// ...
$route = new Route($route_info['path'], $route_info['defaults'], $route_info['requirements'], $route_info['options'], $route_info['host'], $route_info['schemes'], $route_info['methods'], $route_info['condition']);
$collection->add($name, $route);
}
}
// DYNAMIC is supposed to be used to add new routes based upon all the
// static defined ones.
$this->dispatcher->dispatch(new RouteBuildEvent($collection), RoutingEvents::DYNAMIC);
// ALTER is the final step to alter all the existing routes. We cannot stop
// people from adding new routes here, but we define two separate steps to
// make it clear.
$this->dispatcher->dispatch(new RouteBuildEvent($collection), RoutingEvents::ALTER);
$this->checkProvider->setChecks($collection);
$this->dumper->addRoutes($collection);
// ...
$this->dispatcher->dispatch(new Event(), RoutingEvents::FINISHED);
Here we can see the key steps that make Drupal's router more robust than the one provided by Symfony.
- Drupal gets all the route definitions described in YAML configuration throughout the codebase. These get added to a
$collection
variable. - The
rebuild
method uses the EventDispatcher to send (dispatch
) aRoutingEvents::DYNAMIC
event. This enables any contributed module or custom code to register their own dynamic route and add them to the route collection. - Drupal dispatches another event,
RoutingEvents::ALTER
, to allow any other code to alter route definitions compiled thus far. This is akin to amenu_alter
hook in older versions of Drupal (<= Drupal 7). - The
dumper
adds collected route information to a database table using the\Psr\Log\LoggerInterface
. - The
rebuild
method fires off one more event,RoutingEvents::FINISHED
, in case any application code needs to react to the route collection rebuilding.
By making use of the Event system, Drupal is able to support dynamic routes and the altering of existing routes. This makes the system more flexible and helps provide the functionality that the ecosystem of contributed modules have come to expect over the years.
Learn more
Next up
- We'll take a look at what we're terming "utility components". Other Symfony components that are included in Drupal, but that aren't extensively customized by Drupal (if at all).
In Part 2 of our exploration of Symfony components in Drupal, we focus on the event dispatcher.
Spotlight on Symfony in Drupal
- Part 1: HttpKernel in Drupal
- Part 2: EventDispatcher in Drupal
- Part 3: Routing in Drupal
- Part 4: Utility Components in Drupal
The event dispatcher is a tool that enables the application to communicate across objects by subscribing to and listening for events. It achieves this by creating a directory for various event types, and the corresponding registered listeners for each event type. When a specific type of event occurs, the code that has registered a listener for that event is invoked. If you're familiar with the Mediator and Observer design patterns you might recognize similarities here.
Symfony's EventDispatcher component in Drupal
When Drupal receives and incoming request, the HttpKernel
eventually executes this code:
$event = new RequestEvent($this, $request, $type);
$this->dispatcher->dispatch($event, KernelEvents::REQUEST);
This code dispatches RequestEvent
to the EventDispatcher component, along with the $request
object.
Find the code for the EventDispatcher in the vendor/symfony/event-dispatcher/ directory. Let's examine the EventDispatcher.php file in that directory. It contains the class that makes up the core of this component.
The event dispatcher includes methods for adding and removing both listeners and subscribers. It has a dispatch()
method that is responsible for collecting and invoking registered listeners for a particular event.
public function dispatch(object $event, string $eventName = null): object
{
$eventName ??= $event::class;
if (isset($this->optimized)) {
$listeners = $this->optimized[$eventName] ?? (empty($this->listeners[$eventName]) ? [] : $this->optimizeListeners($eventName));
} else {
$listeners = $this->getListeners($eventName);
}
if ($listeners) {
$this->callListeners($listeners, $eventName, $event);
}
return $event;
}
Tip: This code uses ??=
, PHP's null coalescing assignment operator.
The callListeners
method executes the code that has been registered for a specific event.
Drupal's ContainerAwareEventDispatcher class
Drupal optimizes the performance of Symfony's EventDispatcher component. Drupal's customized version, Drupal\Component\EventDispatcher\ContainerAwareEventDispatcher
, runs an initial compilation step which prepares a list of all event listeners and subscribers and stores them in an optimized fashion. Drupal's version is faster by an order of magnitude due to these customizations!
Apart from this additional compiler pass, the 2 components are highly similar.
Learn more
Next up
Stay tuned for our next article in this Spotlight on Symfony series: the Routing component.
For over a decade, Drupal has been using Symfony Components. In 2015, with the release of Drupal 8, these components became a part of Drupal's core software. It's possible to build complex Drupal sites without worrying about what these components do. But learning about the system we're using will make us better developers of Drupal sites and other PHP applications.
In this first part of a 4-part series, we'll explore how Symfony helps Drupal with its HttpKernel component. We'll look at the component itself and how Drupal uses it to coordinate the request/response cycle.
Spotlight on Symfony in Drupal
- Part 1: HttpKernel in Drupal
- Part 2: EventDispatcher in Drupal
- Part 3: Routing in Drupal)
- Part 4: Utility Components in Drupal
Symfony's HttpKernel component in Drupal
One key Symfony component in Drupal is the HttpKernel Component. Its first job is to take the incoming HTTP request from the server, and turn it into a PHP Request
object. This format allows other code to interact with the request without worrying about parsing HTTP. Then, the kernel coordinates the steps to create an HTML response for the browser to display.
To do this, it parses the request and notifies events that convert the Request
object into a Response
.
Drupal's StackedHttpKernel class
Drupal decorates the HttpKernel
in its StackedHttpKernel
(/core/lib/Drupal/Core/StackMiddleware/StackedHttpKernel.php). As you read the code, notice that the StackedHttpKernel
class has 2 main functions: handle
and terminate
.
-
handle
deals with incoming requests. -
terminate
sends the final response to the user.
Here's the truncated class:
/**
...
*/
class StackedHttpKernel implements HttpKernelInterface, TerminableInterface {
// ...
/**
* Constructs a stacked HTTP kernel.
*
* @param \Symfony\Component\HttpKernel\HttpKernelInterface $kernel
* The decorated kernel.
* @param array $middlewares
* An array of previous middleware services.
*/
public function __construct(HttpKernelInterface $kernel, array $middlewares) {
$this->kernel = $kernel;
$this->middlewares = $middlewares;
}
/**
* {@inheritdoc}
*/
public function handle(Request $request, $type = HttpKernelInterface::MAIN_REQUEST, $catch = TRUE): Response {
return $this->kernel->handle($request, $type, $catch);
}
/**
* {@inheritdoc}
*/
public function terminate(Request $request, Response $response) {
$previous = NULL;
foreach ($this->middlewares as $kernel) {
// If the previous kernel was terminable we can assume this middleware
// has already been called.
if (!$previous instanceof TerminableInterface && $kernel instanceof TerminableInterface) {
$kernel->terminate($request, $response);
}
$previous = $kernel;
}
}
}
Symfony's HttpKernel class
Let's examine the handle
method in the HttpKernel
class.
You can find the code for this in vendor/symfony/http-kernel/HttpKernel.php in your Drupal setup. Here's the truncated class:
/**
...
*/
class HttpKernel implements HttpKernelInterface, TerminableInterface
// ...
public function handle(Request $request, int $type = HttpKernelInterface::MAIN_REQUEST, bool $catch = true): Response
{
// ...
$this->requestStack->push($request);
$response = null;
try {
return $response = $this->handleRaw($request, $type);
}
// ...
private function handleRaw(Request $request, int $type = self::MAIN_REQUEST): Response
{
// request
$event = new RequestEvent($this, $request, $type);
$this->dispatcher->dispatch($event, KernelEvents::REQUEST);
if ($event->hasResponse()) {
return $this->filterResponse($event->getResponse(), $request, $type);
}
// ...
}
It uses something called requestStack
to organize the request into a request stack object. Another Symfony component, HttpFoundation creates it. The requestStack
method pushes the current request onto that stack. Then the handleRaw
method creates a RequestEvent
object, and dispatches it to the EventDispatcher
, giving certain events an opportunity to respond to the request. If an event responds to the request, it sends its response, triggering the terminate
method.
Learn more
Dig into the details in our tutorial, How Drupal Turns a Request into a Response, part of our course, Routes and Controllers in Drupal.
Next up
Stay tuned for the next post in our Spotlight on Symfony in Drupal series, where we'll look at the EventDispatcher component and its role in handling requests.
About Drupal Sun
Drupal Sun is an Evolving Web project. It allows you to:
- Do full-text search on all the articles in Drupal Planet (thanks to Apache Solr)
- Facet based on tags, author, or feed
- Flip through articles quickly (with j/k or arrow keys) to find what you're interested in
- View the entire article text inline, or in the context of the site where it was created
See the blog post at Evolving Web