July 12, 2017

Preface

A lot of time has passed since the release of Drupal 8. Instead of using only the hook-oriented paradigm and the procedural programming, Drupal chose a way of involving popular technologies and applying object-oriented methodologies. Changes affected almost all the main parts - from the core functionality to the template engine.

Adding the Symfony components to Drupal 8 had the biggest impact on its development. Drupal became even more flexible than it was before. Developers got a great opportunity to follow the modern technologies and use the object-oriented programming style.

This article focuses on discussing the changes of the common core functionality which were caused by adding the Symfony components. Here you can find the simplified code examples that would help you feel the difference between “clear” Symfony and Drupal 8 solutions. Maybe for some of you this will be the key point to a better understanding of the internal structure of Drupal 8.

Symfony components in Drupal 8

Symfony components in Drupal 8

According to the Symfony documentation, Drupal 8 contains the following components:

  • ClassLoader
  • Console
  • CssSelector
  • DependencyInjection
  • EventDispatcher
  • HttpFoundation
  • HttpKernel
  • Process
  • Routing
  • Serializer
  • Translation
  • Validator
  • Yaml

You should understand that it isn’t a full list because the Symfony community doesn’t track all the changes that have been added to the Drupal 8 core since its release. However, here we can notice the several components which are the basis of Drupal core. I talk about DependencyInjection, EventDispatcher and Routing. The biggest part of changes in the Drupal 8 architecture is connected to the integration of these components. So, the following reasoning is exactly about them.

The basis of Drupal core

Dependency injection and Services in Symfony

Talking about using the DependencyInjection component, it’s impossible not to mention the Service and Service container topics. In general terms, services are any objects managed by the service container. The service container is a special object in which each service lives. This methodology makes usage of services more standardized and flexible. As an additional bonus you get an optimized approach to working with services. If you never ask for some service, it’ll never be constructed. Besides, services are created only once - the same instance is returned every time when you ask for it.

If you have the service container in Symfony, you can easily get some service by its id:

$logger = $container->get('logger');
$entityManager = $container->get('doctrine.orm.entity_manager');

To create a custom service you just need to put a necessary code (most often it’s something that you want to reuse in an application) into a new class. Below there is an example of such a class. It contains a method of getting a random user name from an array.

// src/AppBundle/Service/Lottery.php
namespace AppBundle\Service;

class Lottery
{
    public function getWinner()
    {
        $users = [
            'Alex',
            'John',
            'Paul',
        ];

        $index = array_rand($users);

        return $users[$index];
    }
}

In the services.yml file you can specify how the service container instantiates this service class. It’s possible to specify a large set of parameters here. For additional information you can check this topic.

# app/config/services.yml
services:
    app.lottery:
        class:     AppBundle\Service\Lottery
        arguments: []

That’s it. Your service has the unique key and it’s available in the service container. How about using this service in your controller?

public function newAction()
{
    // ...

    // the container will instantiate a new Lottery()
    $lottery = $this->get('app.lottery');
    $winner = $lottery->getWinner();

    // ...
}

As you may see there is no difference between using the custom services and any of the already existing services.

So, finally, we can discuss the DependencyInjection component itself. It provides several useful classes for operating with services and their dependencies. Let’s create an additional service in order to demonstrate this functionality.

// src/AppBundle/Service/Prize.php
namespace AppBundle\Service;
 
class Prize
{
    protected $lottery;

    public function __construct(\Lottery $lottery)
    {
        $this->lottery = $lottery;
    }

    public function givePrizeToUser()
    {
        // ...
    }
}

The ContainerBuilder class allows you to register the just created class as a service. It can be done in the following way:

use Symfony\Component\DependencyInjection\ContainerBuilder;

$container = new ContainerBuilder();
$container->register('prize', 'Prize');

Our target is adding the dependency between the lottery and prize services. When defining the prize service, the lottery service doesn’t exist yet. You need to use the Reference class to tell the container to inject the lottery service when it initializes.

use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Reference;

$container = new ContainerBuilder();
$container->register('lottery', 'Lottery');
$container
    ->register('prize', 'Prize')
    ->addArgument(new Reference('lottery'));

Dependency injection and Services in Drupal

Drupal slightly extends this scheme. For defining services and additional parameters it uses several yml-files. The core services are defined in the core.services.yml file. Modules can register their own services in the container by creating the modulename.services.yml file in their respective directories. Drupal uses the ServiceProvider class (the CoreServiceProvider.php file) to register the core services.

use Drupal\Core\DependencyInjection\ContainerBuilder;
// ...

class CoreServiceProvider implements ServiceProviderInterface, ServiceModifierInterface {
 
  public function register(ContainerBuilder $container) {
    // ...
   
    $container->addCompilerPass(new ModifyServiceDefinitionsPass());

    $container->addCompilerPass(new ProxyServicesPass());

    $container->addCompilerPass(new BackendCompilerPass());
   
    // ...
  }
 
  // ...
}

This class contains the register() method in which you can find the ContainerBuilder class from the DependencyInjection component. Other modules apply the same principles to register their own services at the service container.

Events and Dispatching Events in Symfony

The dispatching events system is provided by the EventDispatcher component. It includes three parts:

  • Dispatcher - the main object that allows you to register new listeners or subscribers.
  • Listener or Subscriber - the object that you need to connect to the dispatcher in order to stay notified when the event is dispatched.
  • Event - the event class that describes your event.

Let’s try to extend the previous examples. Suppose you want to notify other parts of your application somehow when a random user gets a prize. First of all, you need to define a dispatcher object and connect a listener with this dispatcher.

use Symfony\Component\EventDispatcher\EventDispatcher;

$dispatcher = new EventDispatcher();
$listener = new LotteryListener();
$dispatcher->addListener('lottery.complete.action',array($listener, 'onCompleteAction'));

Then it's worth taking care of your custom event. Custom events are used very often while creating third-party libraries or if you just want to make the whole system more decoupled and flexible.

use Symfony\Component\EventDispatcher\Event;

class LotteryCompleteEvent extends Event
{
    const NAME = 'lottery.complete';

    protected $user;

    public function __construct(Prize $prize)
    {
        $this->prize = $prize;
    }

    public function getPrize()
    {
        return $this->prize;
    }
}

Now each listener has an access to the Prize object via the getPrize() method. During dispatching, you need to pass a machine name for the event. For this reason, the NAME constant is defined inside of the class.

use Symfony\Component\EventDispatcher\EventDispatcher;

$prize = new Prize();
// ...

// Create the LotteryCompleteEvent and dispatch it.
$event = new LotteryCompleteEvent($prize);
$dispatcher->dispatch(LotteryCompleteEvent::NAME, $event);

So, any listener to the lottery.complete event will get the LotteryCompleteEvent object.

In the examples above I used the event listener object, but, actually, this is not a problem to replace it with the subscriber object. The main difference between listeners and subscribers is that the subscriber is able to pass a set of events for subscription to the dispatcher. The subscriber implements the EventSubscriberInterface interface. It requires the single static method called getSubscribedEvents(). In this method you should specify a list of events.

Events and Dispatching Events in Drupal

Regarding appliance the dispatching events mechanism, you wouldn’t find any difference between using it in Symfony and in Drupal 8. Here we just need to talk about using this approach in custom modules and about the possible future plans of Drupal that are related to dispatching events functionality.

To define an Event, Dispatcher and Subscriber classes you need to do the following:

  1. You need to define the Event class under the src/ folder in the module root directory. For instance, let it be src/CustomEvent.php. This class should extend the Event class of the EventDispatcher component.
  2. You need to dispatch this event somewhere. Obviously, it depends on the logic of the module operation.
  3. Under the src/EventSubscriber directory you have to create an Event Subscriber class - src/EventSubscriber/CustomEventSubscriber.php. This class must implement the EventSubscriberInterface interface. It also must contain the single static method called getSubscribedEvents() where you need to define a list of events for subscribing.
  4. Tag the Event Subscriber as ‘event_subscriber’ in the modulename.services.yml file. You can find the necessary information about using tags in the Symfony documentation.

That’s all you need to do for dispatching some events and for reacting to them somehow.

The Events system can be a good replacement for the hook-paradigm which Drupal 8 inherited from the previous version. Despite the fact that many parts have been reworked using events, Drupal 8 still uses hooks. There are some attempts which are related to replacing some well known hooks by events. The Hook Event Dispatcher module is a good example of such an approach.

Routing in Symfony

According to the Symfony documentation you need three main parts for configuring the routing system:

  • RouteCollection - contains the route definitions
  • RequestContext - contains the necessary parameters for the request
  • UrlMatcher - performs the mapping between the request and a single route

These are all classes from the Routing component. For a better understanding let’s look at the simple example:

use Symfony\Component\Routing\Generator\UrlGenerator;
use Symfony\Component\Routing\Matcher\UrlMatcher;
use Symfony\Component\Routing\RequestContext;
use Symfony\Component\Routing\RouteCollection;
use Symfony\Component\Routing\Route;

$route = new Route('/article/{name}', array('_controller' => 'MyController'));
$routes = new RouteCollection();
$routes->add('show_article', $route);
$context = new RequestContext('/');

// Get parameters for the specified route.
$matcher = new UrlMatcher($routes, $context);
$parameters = $matcher->match('/article/my-article');
// array('_controller' => 'MyController', '_route' => 'route_name', 'name' => 'my-article')

// Generate an url from the route.
$generator = new UrlGenerator($routes, $context);
$url = $generator->generate('show_article', array(
    'name' => 'news-article',
));
// /article/news-article

Another important thing that I haven’t added to the list above is the Route class. It allows you to define a single route. Then each route is added to the RouteCollection object. This action is performed by using the RouteCollection::add() method.

The UrlMatcher::match() method returns available parameters for the specified route. If there is no such a route, a ResourceNotFoundException will be thrown.

To generate a url for some route you need to use the UrlGenerator::generate() method. You can pass route variables to this method. For example, it can be useful if you have a wildcard placeholder in the route.

Besides using this approach for configuring the routing system there is a possibility to load routes from a number of the different files. Here everything depends on the FileLocator class. You can use this class to define an array of paths for checking the requested files. For each found file the loader returns a RouteCollection object.

Routing in Drupal

In general, Drupal 8 uses the same mechanisms for working with routes as Symfony does. The Routing component replaced hook_menu() from Drupal 7. Pay attention that the routing system doesn’t work with creation of  tabs, actions and contextual links. So, this functionality which was processed by hook_menu() before is taken over by other subsystems.

To create some routes you need to define them in the modulename.routing.yml file in your module.

class MyRoutesController {  
  // ...
  public function content($category) {
    // Category is a string value.
    // Here can be some logic to process this variable.
  }
}

In this case the system checks permissions for the access to the specified path. If everything is okay, it calls the MyRoutesController::content() method from the controller.

If needed, you can specify a wildcard (also it’s called slug) in a path parameter for your route. It’s only possible to use such wildcards between slashes or after the last slash.

In the example above we specified the category wildcard. Let’s look how you can apply this in the controller:

class MyRoutesController {  
  // ...
  public function content($category) {
    // Category is a string value.
    // Here can be some logic to process this variable.
  }
}

It’s important to use the same name for the variable as it was specified for the wildcard in the path parameter. The search for the corresponding variable is made by its name. It doesn’t even depend on the order in which the variables were specified.

Besides creating your own routes you can alter the already existing ones. This is possible thanks to the RoutingEvents::ALTER event. The event triggers after building routes. To use this functionality you need to extend the RouterSubscriberBase class and implement the alterRoutes(RouteCollection $collection) method of this class.

Conclusion

We considered several important points related to presence of the Symfony components in Drupal 8. Of course, it was not a complete list, but I think these components played a key role in determining the main directions of the Drupal 8 development. Drupal has become much more flexible. Now it's even possible to redefine the behavior of many parts of its core.

Usage of services and dependency injections allows to make your code more flexible and ready to reuse. By applying the DependencyInjection component you get an opportunity of a unified definition of the needed dependencies for your classes. Hooks were partially replaced by the events system from the EventDispatcher component. It allows to get more control over relations between different parts of an application. The routing system from the Routing component replaced the functionality of hook_menu(). It has become more functional, more flexible and more readable.

As you may see the biggest part of solutions in Drupal 8 are heavily based on the mentioned Symfony components. Drupal just extends some approaches and, in some cases, makes them even more flexible. All this represents Drupal 8 more attractive for the Symfony developers. Thus, relationships between Drupal and Symfony communities are strengthened. Based on these facts, we have a good reason to believe that Drupal has chosen the right way for its development. I look forward to see changes in future versions!

Drupal 8 and Symfony

Was this article helpful? Click to rate: 
Average: 4.9 (26 votes)

You may also like

Introduction Sooner or later every developer faces this scary (...
A year ago Dries Buytaert told that those are the content authors and...
Introduction Deployment and configuration management are pretty...