Skip to main content

Injecting Dependencies into Drupal 8 plugins

As part of our code review process for a current project, it was suggested that rather than calling the static Drupal formBuilder function to insert a form into a custom block, we actually inject the *Form Builder service* directly into the module, and for bonus points also inject the renderer service. I'd previously had exposure to dependency injection earlier on the same project but hadn’t exactly grokked the concept fully and so with a few pointers in the right direction, I set about refactoring the code I’d written using dependency injection and Drupal services. 

by alastairmoore /

Dependency Injection is defined as: "A dependency is an object that can be used (a service). An injection is the passing of a dependency to a dependent object (a client) that would use it. The service is made part of the client's state. Passing the service to the client, rather than allowing a client to build or find the service, is the fundamental requirement of the pattern."

Drupal.org defines a service as "any object managed by the services controller". These services provide useful reusable functionality. Examples of services provided by Drupal are the current_user service which gives access to the current user, or email.validator that provides a method of, obviously, validating email addresses. There any many, many services provided by Drupal and the entire list can be found in core.services.yml.

It is also possible to define your own custom services and share them through your own contrib modules, and Drupal.org has a great guide on how to do this and Drupal Console makes it easy to scaffold a service.

Drupal 8 plugins are small pieces of functionality that have mostly replaced the concept of hooks that we used in Drupal 7. A block is now a plugin; field widgets and field types, image formatters and image effects are all plugins.

We’re going to look at using services with a Block plugin and how I injected the Form Builder service into the plugin to embed a form into a custom block.

Firstly we need to define a Block plugin, which was scaffolded using the Drupal Console command drupal generate:plugin:block.

namespace Drupal\example\Plugin\Block;

use Drupal\Core\Block\BlockBase;

/**
 * @Block( * id = "example_form_block", 
 * admin_label = @Translation("Example Form Block"), 
 * ) 
 */
 class ExampleFormBlock extends BlockBase {
   public function build() {
     $build = [];
     $build['example_form_block']['#markup'] = 'Implement ExampleFormBlock.';
     return $build;
   }
}

This will create a simple block that does nothing but output "Implement ExampleFormBlock" as its content, however we want our block to do a little more than just that.

Using Drupal Console again, we can simply scaffold a basic contact form using the command drupal generate:form, which contains nothing more than name and message fields.

class ExampleForm extends FormBase {

  public function getFormId() {
    return 'example_form';
  }

  public function buildForm(array $form, FormStateInterface $form_state, $placeholder = NULL) {

    $form['name'] = [
      '#title' => $this->t('Name'),
      '#type' => 'textfield',
      '#maxlength' => 64,
      '#size' => 64,
    ];

    $form['message'] = [
      '#title' => $this->t('Message'),
      '#type' => 'textarea',
      '#rows' => 5,
    ];

    $form['submit'] = [
      '#type' => 'submit',
      '#value' => $this->t('Submit'),
    ];

    return $form;
  }    
}

This form won't actually do anything but works well as an example.

To use the form builder service or, in fact, any service, we need to inject that dependency into our plugin and we can do this by implementing the ContainerFactoryPluginInterface in our block plugin. The ContainerFactoryPluginInterface allows plugins to pull dependencies from the service container and provides a create method to our plugin which we can use to inject whichever services we require.

We also need to create a constructor method to store the services we wish to inject in class properties. Updating our code to do this looks like the following:

namespace Drupal\example\Plugin\Block;

use Drupal\Core\Block\BlockBase;
use Drupal\Core\Form\FormBuilderInterface;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;

/**
 * Provides a 'ExampleFormBlock' block.
 *
 * @Block(
 *  id = "example_form_block",
 *  admin_label = @Translation("Example Form Block"),
 * )
 */
class ExampleFormBlock extends BlockBase implements ContainerFactoryPluginInterface {

  protected $formBuilder;

  public function __construct(array $configuration, $plugin_id, $plugin_definition, FormBuilderInterface $formBuilder) {
    parent::__construct($configuration, $plugin_id, $plugin_definition);
    $this->formBuilder = $formBuilder;
  }

  public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
    return new static(
      $configuration,
      $plugin_id,
      $plugin_definition,
      $container->get('form_builder')
    );
  }

  public function build() {
    $build = [];
    $build['example_form_block']['#markup'] = 'Implement ExampleFormBlock.';
    return $build;
  }

}

And simple as that, we've injected the Form Builder service into our Block plugin. You could of course inject other services into the plugin, for example the Renderer service, by creating additional class properties, and adding the service to the services container as we did with injecting form_builder.

We are now able to utilise the Form Builder service in our plugin, and in this example, we're going to use the service to get our previously scaffolded form. Because the Form Builder service is now available, it's as simple as updating the build() method in our Block plugin to call the getForm() method:

public function build() {
  $form = $this->formBuilder->getForm('Drupal\example\Form\ExampleForm');
  return $form;
}

On refreshing the page our custom block has been placed on, you will see our contact form embedded in the block.

Wrapping up

As you can see, dependency injection in Drupal 8 is not difficult to implement with plugins and offers a powerful way of taking advantage of services provided by Drupal core and also contrib modules. You can read up on more advanced topics with Drupal services and dependency injection in a blog post by Lee Rowlands.

Posted by alastairmoore
Drupal Developer

Dated

Comments

Comment by Joachim

Dated

Great overview, thanks!

Just wanted to add that you can use Module Builder to generate the boilerplate code for injecting services into plugins, forms, and other services.

Comment by just-passin-thru

Dated

This is a great example! But the lingering question I have is WHY I would want to do this as opposed to just building the form in the block plugin the 'old-fashioned' way? What's the value of this pattern?

Comment by drupal

Dated

I would say that injected:

$this->formbuilder->getForm()

is easier to test than:

\Drupal::service('formBuilder)->getForm()

Comment by Moshe Weitzman

Dated

The brief answer is "for testability". Done this way, ExampleFormBlock gets handed all its dependencies whenever it is contructed so you unit test it independently of anything else.

Whether the above benefit outweighs the complexity of dependency injection is a whole other discussion, which never finishes. I'm not convinced myself.

Comment by Purencool

Dated

Thanks, this is a great tutorial. Quick question can you use build method to access contact forms an admin creates in site structure?

Pagination

Add new comment

Restricted HTML

  • Allowed HTML tags: <a> <em> <strong> <cite> <blockquote> <code> <ul> <ol> <li> <dl> <dt> <dd> <h2> <h3> <h4> <h5> <h6>
  • Lines and paragraphs break automatically.
  • Web page addresses and email addresses turn into links automatically.
Not sure where to start? Try typing "hello" or "help" if you get stuck.