Drupal Symfony

Drupal and Symfony

A lot of time has passed since the release of Drupal 8. Instead of using only the hook-oriented paradigm and 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.

→ This post is about an older version of Drupal. It can still be useful to you but you can also check out our latest article about Drupal 10.

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

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

symfony drupal
Drupal 8 and Symfony components

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

Note that it isn’t the complete list because the Symfony community doesn’t track all the changes made to the Drupal 8 core since its release. However, here we can notice several components that act as the basis of Drupal core. I talk about Dependency Injection, Event Dispatcher, and Routing. The majority of changes in the Drupal 8 architecture are connected to the integration of these components.

symfony dependency injection
What Symfony components does Drupal 8 use?

Symfony Dependency Injection

When discussing the DependencyInjection component, it’s impossible not to mention the Service and Service container topics. In summary, services are any objects managed by a service container. A service container is a special object in which each service lives. This methodology makes the 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 whenever 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 is an example of such a class. It contains a method of getting a random username 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 a 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 custom services and any of the already existing services.

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 the following way:

use Symfony\Component\DependencyInjection\ContainerBuilder;
$container = new ContainerBuilder();
$container->register('prize', 'Prize');

Our target is to add the dependency between the lottery and prize services. When defining the prize service, the lottery service doesn’t exist yet. You must 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'));

Drupal Dependency Injection

Drupal slightly extends this scheme. It uses several yml-files for defining services and additional parameters. 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.

Symfony Events and Dispatching Events

The dispatching events system is provided by the EventDispatcher component. It has 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. Let's imagine that you want to notify other parts of your application 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 this is actually 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 the subscription to the dispatcher. The subscriber implements the EventSubscriberInterface interface. It requires a single static method called getSubscribedEvents(). In this method, you should specify a list of events.

Drupal Events and Dispatching Events

There is no difference between using the dispatching events mechanism 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 the dispatching events functionality.

To define the 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.

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 to replace some well-known hooks with events. The Hook Event Dispatcher module is a good example of such an approach.

Symfony Routing

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

  • RouteCollection contains the route definitions.
  • RequestContext .ontains 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 a 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 route, a ResourceNotFoundException will be thrown.

To generate a URL for a 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 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.

Drupal Routing

In general, Drupal 8 uses the same mechanism for working with routes as Symfony does. The Routing component replaced hook_menu() from Drupal 7. Pay attention to the fact that the routing system doesn’t work with the creation of tabs, actions, and contextual links. 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 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 a 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 at how you can apply this in a 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.

symfony routing
Symfony and Drupal 8

Conclusion

We considered several important points related to the presence of 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.

The usage of services and dependency injections allows you to make your code more flexible and easy 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 with the events system from the EventDispatcher component. It allows one 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, flexible, and readable.

As you may see, the majority 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 makes Drupal 8 more attractive for Symfony developers. Thus, relationships between Drupal and Symfony communities are strengthened. Based on these facts, we have good reason to believe that Drupal has chosen the right way for its development. I look forward to seeing more changes in future versions!

There we described the creation of a CRM application on this framework.

We’re always here for you to provide you with advice and chat.

More on the topic

You might also like