Dependency Injection in a form

Last updated on
1 February 2026

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 10+) 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 {

  /**
   * @param \Drupal\Core\Session\AccountInterface $account
   */
  public function __construct(protected readonly AccountInterface $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. Note that this example uses constructor property promotion, which was introduced in PHP8. For older versions of PHP it was necessary to declare properties separately as class variables.

The create method is part of the Drupal\Core\DependencyInjection\ContainerInjectionInterface, which allows forms (and other classes created by Drupal) to be instantiated with services from the container. 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\Config\ConfigFactoryInterface;
use Drupal\Core\Form\ConfigFormBase;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Session\AccountProxyInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;

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

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

  /**
   * {@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();
    $this->messenger()->addStatus($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);
  }
}

@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 in blocks

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;
  }

}

Service autowiring in forms

Since Drupal 10.2, you can use the AutowireTrait trait to automatically infer services from their interfaces.

Drupal 10 and 11 note: This pattern is available in Drupal 10.2+ and continues in Drupal 11. Use it when it improves readability, but prefer explicit wiring when you need strict control over which service ID is used.

The ExampleConfigForm example from earlier could be simplified as follows:

<?php

namespace Drupal\example\Form;

use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\DependencyInjection\AutowireTrait;
use Drupal\Core\Form\ConfigFormBase;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Session\AccountProxyInterface;

class ExampleConfigForm extends ConfigFormBase {

  use AutowireTrait;

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

This allows the create function to be completely removed, simplifying the class and removing explicit dependencies.

In cases where multiple services share the same interface, we need to provide some additional help. We can do this by adding an Autowire attribute to the constructor parameter. For example, several caching services use  CacheBackendInterface, so if we wanted to add the render cache to the above class, we could use:

<?php

namespace Drupal\example\Form;

use Drupal\Core\Cache\CacheBackendInterface;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\DependencyInjection\AutowireTrait;
use Drupal\Core\Form\ConfigFormBase;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Session\AccountProxyInterface;
use Symfony\Component\DependencyInjection\Attribute\Autowire;

class ExampleConfigForm extends ConfigFormBase {

  use AutowireTrait;

  /**
   * Class constructor.
   */
  public function __construct(
    ConfigFactoryInterface $config_factory, 
    protected readonly AccountProxyInterface $currentUser,
    #[Autowire(service: 'cache.render')]
    protected readonly CacheBackendInterface $cacheRender,
  ) {
    parent::__construct($config_factory);
  }
…

This will ensure the correct service gets associated with the property.

Considerations for serialization

When form objects are serialized by Drupal, private service properties can be 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 callback runs and processes 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 (use protected or public visibility).

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

Example:

class MyEntityForm extends EntityForm 

  /**
   * 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(protected readonly RouteBuilderInterface $routeBuilder) {}

}

Help improve this page

Page status: Needs review

You can: