Handling many services

Published at

March 17, 2020

Author

Erlend ter Maat

Tags

Drupal, Dependency injection, Maintainable code


In my previous blog post I explained a feature that is introduced in Drupal 8: Dependency injection. There is no limit in the number of services you can make another service dependent of. This may lead to long lists of services. It can become hard to understand the business case that this implemented by the service/form/plugin/whatever. This is where the class resolver can be useful.

A working example of the principles in this blog post is available here. Below I show small snippets of the code that you can find in this repo.

Organize services in custom forms

Code where you are easily tempted to write one or more very large functions is when creating forms. Next example shows how you can keep code readable.

class CustomMultistepForm extends FormBase {

  public static function create(Container $container) {
    return new static(
      $container->get('class_resolver'),
      $container->get('tempstore.private')
    );
  }

  protected $classResolver;

  protected $tempStore;

  public function __construct(ClassResolverInterface $classResolver
                              PrivateTempStoreFacory $tempStoreFactory) {
    $this->classResolver = $classResolver;
    $this->tempStore = $tempStoreFactory->get(static::class);
  }

}

The form we are building stores its context in a private tempstore. I found in this a better way of storing form data over subsequent web requests. I had issues with storing data on the FormState, and storing data in an object that is intended for storing session data temporally seems a better practice.

public function buildForm(array $form, FormStateInterface $form_state) {
  $builder = $this->getPageBuilder();

  // Use the builder to populate the $form array.
  // ...

  return $form;
}

protected function getPageBuilder() {
  switch($this->tempStore->get('current_page')) {
    default:
    case CustomStep1FormBuilder::ID:
      return $this->classResolver->getInstanceFromDefinition(
        CustomStep1FormBuilder::class
      );

    case CustomStep2FormBuilder::ID:
      return $this->classResolver->getInstanceFromDefinition(
        CustomStep2FormBuilder::class
      );
  }
}

The form is built using a separate class for each page. When a service is only used for one page, the service dependency only has to be defined at the service that uses the dependency.

class CustomStep1FormBuilder implements CustomFormBuilderInterface {

  public static function create(ContainerInterface $container) {
    return new static($container->get('current_user'));
  }

  protected $currentUser;

  public function __construct(AccountProxyInterface $currentUser) {
    $this->currentUser = $currentUser;
  }

}

Conclusion

The class resolver facilitates the inversion of control principle and, in my opinion, it could be categorized as a way to give your code a single responsibility. Code gets cleaner and more maintainable for when you isolate dependencies from the higher level code, then as little as possible code is required to change when business rules change.