Dependency Injection for a Form

Last updated on
3 December 2022

This documentation needs review. See "Help improve this page" in the sidebar.

Forms that require a Drupal service or a custom service should access the service using dependency injection.

An example form (similar to the form used in Form API in Drupal 8) uses the 'current_user' service to get the uid of the current user. File contents of /modules/example/src/Form/ExampleForm.php if the module is in /modules/example:

<?php

namespace Drupal\example\Form;

use Drupal\Core\Form\FormBase;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Session\AccountInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;

/**
 * Implements an example form.
 */
class ExampleForm extends FormBase {

  /**
   * @var \Drupal\Core\Session\AccountInterface
   */
  protected $account;

  /**
   * @param \Drupal\Core\Session\AccountInterface $account
   */
  public function __construct(AccountInterface $account) {
    $this->account = $account;
  }

  /**
   * {@inheritdoc}
   */
  public static function create(ContainerInterface $container) {
    // Instantiates this form class.
    return new static(
      // Load the service required to construct this class.
      $container->get('current_user')
    );
  }

  /**
   * {@inheritdoc}.
   */
  public function getFormId() {
    return 'example_form';
  }

  /**
   * {@inheritdoc}.
   */
  public function buildForm(array $form, FormStateInterface $form_state) {

    // Get current user data.
    $uid = $this->account->id();
    
    // ...
  }
  
  /**
   * {@inheritdoc}
   */
  public function submitForm(array &$form, FormStateInterface $form_state) {
    // ...
  }
}

The create method is a factory method that returns a new instance of the ExampleForm object. The ExampleForm::create loads one or more services. This can be any core service, defined in core.services.yml or any *.services.yml file.

ExampleForm::__construct uses the services that are loaded by ExampleForm::create and stores them in properties of the class. The order in which the services are loaded in ExampleForm::create must be equal to the order of the parameters in the ExampleForm::__construct method.

The create method is part of the Drupal\Core\DependencyInjection\ContainerInjectionInterface which allows controllers to be instantiated with a service. Drupal\Core\Form\FormBase already implements this interface. Any form that extends Drupal\Core\Form\FormBase, such as ConfigFormBase and ConfirmFormBase, has this ability of Dependency Injection.

<?php

namespace Drupal\example\Form;

use Drupal\Core\Form\ConfigFormBase;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Config\ConfigFactoryInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Drupal\Core\Session\AccountProxyInterface;

/**
 * Implements an example configuration form.
 */
class ExampleConfigForm extends ConfigFormBase {

  /**
   * Drupal\Core\Session\AccountProxyInterface definition.
   *
   * @var AccountProxyInterface $currentUser
   */
  protected $currentUser;

  /**
   * Class constructor.
   */
  public function __construct(ConfigFactoryInterface $config_factory, AccountProxyInterface $current_user) {
    parent::__construct($config_factory);
    $this->currentUser = $current_user;
  }

  /**
   * {@inheritdoc}
   */
  public static function create(ContainerInterface $container) {
    return new static(
      $container->get('config.factory'),
      $container->get('current_user')
    );
  }

  /**
   * {@inheritdoc}
   */
  protected function getEditableConfigNames() {
    return [
      'example.config_form',
    ];
  }

  /**
   * {@inheritdoc}.
   */
  public function getFormId() {
    return 'example_config_form';
  }

  /**
   * {@inheritdoc}.
   */
  public function buildForm(array $form, FormStateInterface $form_state) {

    // Get current user data.
    $uid = $this->currentUser->id();
    drupal_set_message($uid);

    // ...

    $config = $this->config('example.config_form');
    return parent::buildForm($form, $form_state);
  }

  /**
   * {@inheritdoc}
   */
  public function submitForm(array &$form, FormStateInterface $form_state) {
    $this->config('example.config_form')->save();
    parent::submitForm($form, $form_state);
  }
}

Above example using constructor property promotion (PHP 8.0):

…
class ExampleConfigForm extends ConfigFormBase {

  /**
   * Class constructor.
   */
  public function __construct(
    ConfigFactoryInterface $config_factory, 
    protected AccountProxyInterface $currentUser
  ) {
    parent::__construct($config_factory);
  }

  /**
   * {@inheritdoc}
   */
  public static function create(ContainerInterface $container) {
    return new static(
      $container->get('config.factory'),
      $container->get('current_user')
    );
  }
…

@todo Show how FormController can automatically insert $route info into your buildForm() method by simply defining a parameter with the same class interface.

Example

buildForm(array $form, FormStateInterface $form_state, Request $request = NULL) 

Dependency Injection Inside Block (Plugin)

We can inject our custom/core service inside our custom block plugin for that we have to use: 

use Drupal\Core\Plugin\ContainerFactoryPluginInterface;

Suppose you have a custom block with the name 'drupalise' then you inject your custom service 'drupalise' inside your custom block code as follows:

<?php

namespace Drupal\drupalise\Plugin\Block;

use Drupal\Core\Block\BlockBase;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Drupal\drupalise\Services\DrupaliseMe;

/**
 * Provides a 'Drupalise' block.
 *
 * @Block(
 * id = "drupalise",
 * admin_label = @Translation("Drupalise"),
 * )
 */
class Drupalise extends BlockBase implements ContainerFactoryPluginInterface {

  /**
   * @var $drupalise \Drupal\drupalise\Services\DrupaliseMe
   */
  protected $drupalise;

  /**
   * @param \Symfony\Component\DependencyInjection\ContainerInterface $container
   * @param array $configuration
   * @param string $plugin_id
   * @param mixed $plugin_definition
   *
   * @return static
   */
  public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
    return new static(
      $configuration,
      $plugin_id,
      $plugin_definition,
      $container->get('drupalise')
    );
  }

  /**
   * @param array $configuration
   * @param string $plugin_id
   * @param mixed $plugin_definition
   * @param \Drupal\drupalise\Services\DrupaliseMe $drupalise_me
   */
  public function __construct(array $configuration, $plugin_id, $plugin_definition, DrupaliseMe $drupalise_me) {
    parent::__construct($configuration, $plugin_id, $plugin_definition);
    $this->drupalise = $drupalise_me;
  }

  /**
   * {@inheritdoc}
   */
  public function build() {
    $slogan= $this->drupalise->Drupalise(); // Function from your service file will return 'Drop Drop Drupal !!!'
    $build = [];
    $build['drupalise']['#markup'] = 'Drupalise is best' . $slogan;
    return $build;
  }

}

Considerations for Serialization

When service objects are seralized by drupal private properties are lost.

Be sure to not set any service properties in your form class to have private visibility.

For example, when a form is rebuilt via ajax as in when you add an #ajax definition to a form element, the form class is serialized on
ajax requests and any services that are assigned to private properties in the class are lost. This results in a form where the ajax runs and proceses successfully then generates errors due to null property values on subsequent ajax requests.

When you have an element with a defined #ajax handler like the below, you'll need to ensure the services injected into your form class
are not private.

      '#ajax' => [
        'callback' => '::myAjaxCallback', 'event' => 'change', 'wrapper' => 'my-ajax-wrapper', 'progress' => [
          'type' => 'throbber', 'message' => $this->t('Verifying entry...'),
        ],
      ]

Example:

class MyEntityForm extends EntityForm 

  /**
   * Drupal routeBuilder object, for rebuilding routes on form save.
   *
   * This is an injected service and must not be declared private or
   * it will be lost on serialisation of this form object.
   *
   * @var \Drupal\Core\Routing\RouteBuilder
   */
  protected $routeBuilder

  /**
   * MyEntityForm constructor.
   *
   * The injected service properties, eg $this->routeBuilder must
   * be not be declared private as this form is reloaded with ajax and
   * cached, private properties aren't serialisable.
   *
   * @param \Drupal\Core\Routing\RouteBuilderInterface $routeBuilder
   */
  public function __construct(RouteBuilderInterface $routeBuilder) {
    $this->routeBuilder = $routeBuilder
  }

}

Help improve this page

Page status: Needs review

You can: