Module specific services

Since Fork CMS version 3.9.2, our Fork modules look a little more like Symfony Bundles. They can now contain a so called "module extension" class that can be used to load module specific configuration or services.

What is an extension class?

For Symfony developers, it is important to write code with a high code quality. A way to achieve this nice quality is decoupling your code into small classes that have only one responsibility. They can be called Services and can do simply anything. The definition of a service, as stated in the Symfony documentation looks like this:

Put simply, a Service is any PHP object that performs some sort of "global" task.

In Fork, we already have some global services available: the database service, the fork.settings service to fetch and store modulesettings, the fork.response_securer to add security related headers to our responses and a lot more. There are also Symfony specific services, such as logger, available. These services live in a service container, which can be seen as a big array containing all these services, but with some nice extra features (for example lazy loading).

We fetch these services from the service container. Doing this is as easy as this:

// fetching a service in an action or widget
$service = $this->get('<name-of-the-service>');

// fetching a service in another place
use Frontend\Core\Engine\Model;
$service = Model::get('<name-of-the-service>');

If you're used to developing in Fork, you probably recognize this from fetching the database.

Wouldn't it be nice to be able to create services like this, but for every module, and only get them loaded in the service container if their corresponding module is installed? That's exactly what a module extension does! It lets you automatically load extra configuration and services whenever the module has been installed.

How does it work?

Your module extension is just a PHP object that has to live in a specific directory to be automatically loaded. I'll use an imaginary Events module as example to show how it works. In the backend of your module (src/Backend/Modules/Events), you should create a new directory, called "DependencyInjection". Your extension class should live in there and should have the name of your module suffixed with "Extension" as name. In our example, that would be src/Backend/Modules/Events/DependencyInjection/EventsExtension.php.

A module extension is a really simple class. It should extend the Extension class from the HttpKernel component and should contain a load method. This one will be called to create all extra services. It looks like this (for our Events module).

<?php

namespace Backend\Modules\Events\DependencyInjection;

use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\Config\FileLocator;
use Symfony\Component\HttpKernel\DependencyInjection\Extension;
use Symfony\Component\DependencyInjection\Loader;

/**
 * This is the class that loads and manages the module configuration
 * for the Events module
 */
class EventsExtension extends Extension
{
    /**
     * {@inheritDoc}
     */
    public function load(array $configs, ContainerBuilder $container)
    {
        $loader = new Loader\YamlFileLoader($container, new FileLocator(__DIR__.'/../Resources/config'));
        $loader->load('services.yml');
    }
}

As you can see, this class will create a YamlFileLoader that will load files in the <module>/Resources/config directory. It specifically asks for the services.yml file.

This will just throw an exception when you try to use this without adding this services.yml file. To make sure it works, we'll create the directory /Resources/config in our module. In there, we create a services.yml file. This file could look something like this:

# src/Backend/Modules/Events/Resources/config/services.yml
services:
    events.subscription_notifier:
        class: Frontend\Modules\Events\Service\SubscriptionNotifier
        arguments:
            - @database
            - @mailer

In this example, we load a service with the name 'events.subscription_notifier, with a specified class. This class will receive the database and the mailer object in it's constructor. This means that we can use the database and mailer service inside our newly created service, without needing to call the service container.

There are really lots of possibilities to create services. You can read all about it here: http://symfony.com/doc/current/book/service_container.html#creating-configuring-services-in-the-container.

Are there working examples in Fork CMS?

The FormBuilder module and the new Analytics module both contain this Extension class and use this to load their module specific services. The Analytics module for example loads an 'analytics.connector' service that connects to Google Analytics and fetches data from it.

If you want to see if your services are registered in the container, you can use the app/console container:debug command on your command line. This will output all the registered services. Note that it could be necessary to clear your cache once in a while to get your services in the container, since Symfony caches the container for performance reasons.

This should be enough to get you started creating services and loading them in your modules. Have fun experimenting with them!