Voici un retour d’expérience sur un cas assez courant dans le développement de code custom pour Drupal 8.
Un module contient un contrôleur dont vous voulez surcharger une des méthode parce que vous avez besoin de modifier la logique de celle-ci et que le code ne vous fourni aucun moyen pour ça (un hook par exemple).
Mais cette classe qui implémente ControllerBase fait déjà usage de l'injection de dépendance, (et de ce fait le plus souvent annule tout ou partie des méthode disponibles avec ControllerBase, mais c'est un autre sujet).
Un exemple concret avec le module Message subscribe, le sous module message_subscribe_ui utilise un contrôleur SubscriptionController, une des méthode getView()
permet d'appeler une vue (views) pour afficher des pages d'administration.
Problème, le module se base sur une règle de nom de la vue avec nom d'un flag :
$prefix = $this->config->get('flag_prefix');
// View name + display ID.
$default_view_name = $prefix . '_' . $entity_type . ':default';
Seulement voila, pour divers raison cela ne me convient pas, dans mon cas actuel j'utilise PostreSQL et les vues utilisés ne fonctionnent pas ! Je souhaite donc modifier ce code et surcharger cette méthode pour utiliser des vues différentes avec ma propre règle de nommage.
Le contrôleur utilise la méthode tab()
pour créer un onglet de page qui ensuite appelle getView()
pour afficher la vue.
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) {
// Logique pour retourner la vue.
return $view;
}
La solution va donc être de surcharger dans Drupal cette méthode tab()
et de pouvoir ainsi modifier getView()
, pour cela je doit utiliser RouteSubscriber pour modifier la route et le contrôleur associé comme expliqué dans la documentation Drupal.
La première étape est d'avoir un nouveau service avec le fichier my_module.services.yml
:
services:
my_module.route_subscriber:
class: Drupal\my_module\Routing\RouteSubscriber
tags:
- { name: event_subscriber }
Et de remplacer le contrôleur de la 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');
}
}
}
Maintenant je peux mettre en place ma classe, mais j'ai besoin d'injecter un nouveau service pour mon code, pour y arriver je dois utiliser l'injection de dépendance déjà existante dans le constructeur de la classe et ajouter mon service.
C'est là qu'on ne trouvera pas beaucoup de documentation, voici mon exemple avec les commentaires réduits :
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 entotyTypeManager
$storage = $this->entityTypeManager->getStorage('flagging');
$query = $storage->getQuery();
*/
return $view;
}
}
Plutôt simple au final, il suffit d'appeler le constructeur parent depuis mon constructeur et d'ajouter mon service et d'ajouter celui-ci dans ma méthode create()
.