Image

Dependency injection when extending a class that implements ControllerBase

Here is an almost typical use case when developing custom code for Drupal 8.

A third party module declare a controller with a method that you want to override because you need to change the logic and there is no practical way to do it in the module (could be a hook).

But this class implements ControllerBase with dependency injection, (and override most of the methods in ControllerBase, but this is an other subject).

A concrete example with module Message subscribe, included sub module message_subscribe_ui declare a controller SubscriptionController, it include a method getView() which call and build a view for the administration pages.

Problem is the controller find a view based on a name with a flag:

$prefix = $this->config->get('flag_prefix');
// View name + display ID.
$default_view_name = $prefix . '_' . $entity_type . ':default';

And here is my concern, it's not good for me, in my case I am using PostreSQL and the views simply do not work! I need to override this code to add my own naming convention or custom code to build the page.

The controller declare method tab() to create a tab for our pages and then call method getView() to display the content.

public function tab(UserInterface $user, FlagInterface $flag = NULL) {
/* ... */
  $view = $this->getView($user, $flag);
  $result = $view->preview();
/* ... */
  return $result;
}
/* ... */
protected function getView(UserInterface $account, FlagInterface $flag) {
  // Logic to find and return the view...
  return $view;
}

My solution is to override in Drupal this method tab() and alter getView(), to do it I need to use RouteSubscriber to alter the class used by the controller as you can see in the Drupal documentation.

First step is to create a service using event_subscriber tag in a file my_module.services.yml

services:
  my_module.route_subscriber:
    class: Drupal\my_module\Routing\RouteSubscriber
    tags:
      - { name: event_subscriber }

And replace the controller for this route

namespace Drupal\my_module\Routing;

use Drupal\Core\Routing\RouteSubscriberBase;
use Symfony\Component\Routing\RouteCollection;

/**
 * Listens to the dynamic route events.
 *
 * @TODO: Remove when https://www.drupal.org/project/flag/issues/2864440 fixed
 */
class RouteSubscriber extends RouteSubscriberBase {

  /**
   * {@inheritdoc}
   */
  protected function alterRoutes(RouteCollection $collection) {
    if ($route = $collection->get('message_subscribe_ui.tab.flag')) {
      $route->setDefault('_controller', '\Drupal\my_module\Controller\SubscriptionController::tab');
    }
  }
}

Now I can override and write my class with my new methods, but in this case I need to use a service not available in the original controller and to do so use the dependency injection with the constructor of the parent class.

There is not much documentation for this use case, here is my example without comments

namespace Drupal\my_module\Controller;

use Drupal\message_subscribe_ui\Controller\SubscriptionController as BaseSubscriptionController;

use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\Session\AccountProxyInterface;
use Drupal\flag\FlagInterface;
use Drupal\flag\FlagServiceInterface;
use Drupal\message_subscribe\Exception\MessageSubscribeException;
use Drupal\message_subscribe\SubscribersInterface;
use Drupal\user\UserInterface;
use Drupal\views\Views;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;

/**
 * Overridden controller for the message_subscribe_ui module.
 *
 * We alter the getView() method.
 *
 * @TODO: Remove when https://www.drupal.org/project/flag/issues/2864440 fixed
 */
class SubscriptionController extends BaseSubscriptionController {

  /**
   * The entity type manager.
   *
   * @var \Drupal\Core\Entity\EntityTypeManagerInterface
   */
  protected $entityTypeManager;

  /**
   * Construct the subscriptions controller from parent.
   * ...
   */
  public function __construct(AccountProxyInterface $current_user, FlagServiceInterface $flag_service, SubscribersInterface $subscribers, ConfigFactoryInterface $config_factory, EntityTypeManagerInterface $entity_manager) {
    parent::__construct($current_user, $flag_service, $subscribers, $config_factory);
    // Simply add our service.
    $this->entityTypeManager = $entity_manager;
  }

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

  /**
   * Render the subscription management tab.
   *
   * ...
   */
  // @codingStandardsIgnoreStart
  public function tab(UserInterface $user, FlagInterface $flag = NULL) {
    // Keep the original, we only want to replace getView().
    return parent::tab($user, $flag);
  }
  // @codingStandardsIgnoreEnd 

  /**
   * Helper function to get a view associated with a flag.
   * ...
   */
  protected function getView(UserInterface $account, FlagInterface $flag = NULL) {
    /*  My logic with entityTypeManager
    $storage = $this->entityTypeManager->getStorage('flagging');
    $query = $storage->getQuery();
    */
    return $view;
  }

}

As you can see it's simple, I use the original __construct() and add my service. Then I add my service again in the create().

Comments