Utiliser le DIC de Symfony dans Drupal 7

Afin de préparer la compatibilité vers Drupal 8, il est possible de créer des composants avec le Dependency Injection Container de Symfony dans Drupal 7

Le blog Makina-corpus

Afin de préparer la compatibilité vers Drupal 8, il est possible de créer des composants avec le Dependency Injection Container de Symfony dans Drupal 7

Lors du développement de Drupal 8, la communauté est enfin sortie de sa bulle : des expressions comme "Getting off the island", "Not invented here" ou "Proudly found elswhere" sont apparues et représentent toutes cet objectif de ne pas réinventer la roue et d'incorporer des composants robustes et testés déjà développés par la communauté PHP.

L'utilisation de Composer a donc permis cette intégration de différents composants tels que Guzzle, PsrLog, Twig, ZendFeed, Behat, password_compat, EasyRDF, EmailValidator et quelques uns de Symfony et Doctrine.

Ces composants sont utilisés et peuvent être surchargés par le principe de services. Le principe est de déclarer des services sous forme de fichier Yaml, en leur attribuant un nom système.


Dependency Injection Container


Compatibilité avec Drupal 7

Imaginons maintenant que vous ou votre client a dans l'optique de migrer vers Drupal 8 dans les prochaines années, vous souhaitez donc développer des modules en mode composant, qui soient réutilisables dans Drupal 8. Il va falloir donc respecter les interfaces de celui-ci, et pour cela il nous faut utiliser la même méthode, l'injection de dépendance.

Voici par exemple un extrait de fichier `poney.services.yml` qui déclare le service poney.manager, ce service s'occupera de gérer nos poneys :

services:
  poney.manager:
    class: Poney\Manager
    arguments: ["@database"]

Pour gérer les objets de poney, il doit d'interfacer avec la base de données, que l'on passe donc en paramètre.

Grâce au module Symfony DIC il est possible de déclarer ceci dans Drupal 7, ce qui fonctionnera également dans Drupal 8.


Liste des services de D8 portés en D7

La liste de services inclus dans Drupal 8 est assez suffisante et les pull-requests bienvenues :

  • service_container
  • kernel
  • http_kernel
  • request_stack
  • current_user
  • database
  • form_builder
  • module_handler
  • cache.bootstrap
  • cache.default
  • cache.entity
  • cache.menu
  • cache.render
  • cache.data
  • entity.manager
  • path.alias_manager
  • path.alias_storage
  • path.current
  • event_dispatcher
  • logger.factory
  • session

Utilisation de la CompilerPass

Dès que l'on a le DIC de Symfony, on peut utiliser la CompilerPass pour injecter au runtime des services par des modules activés ou non. Il suffit de créer un fichier poney.container.php :

<?php

namespace Drupal\Module\poney;

use Drupal\Core\DependencyInjection\ServiceProviderInterface;
use MakinaCorpus\Poney\DependencyInjection\Compiler\CompilerPass;
use Symfony\Component\DependencyInjection\ContainerBuilder;

class ServiceProvider implements ServiceProviderInterface
{
    /**
     * {@inheritdoc}
     */
    public function register(ContainerBuilder $container)
    {
        $container->addCompilerPass(new CompilerPass());
    }
}
<?php

namespace MakinaCorpus\Poney\DependencyInjection\Compiler;

use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Reference;

class CompilerPass implements CompilerPassInterface
{
    public function process(ContainerBuilder $container)
    {
        if ($container->hasDefinition('etable')) {
            $taggedServices = $container->findTaggedServiceIds('etable');
            foreach ($taggedServices as $id => $attributes) {
                $formatter = $container->getDefinition($id);
                $formatter->addMethodCall(
                    'setEtable',
                    [new Reference('etable')]
                );
            }
        }
    }
}

Puis lorsqu'un module tiers sera activé et fournira un service etable, la méthode setEtable des services ayant le tag etable sera automatiquement appelée.


Surcharge de services

Lorsqu'on veut surcharger un service pour modifier son fonctionnement, il est possible de le décorer, par exemple :

monautremodule.etalon:
  class: MakinaCorpus\MonAutreModule\Etalon
  public: false
  decorates: poney   

Créer des Contrôleurs et des Formulaires Drupal 8


Création d'un contrôleur simple

Les controleurs dans Drupal 8 sont l'équivalent en Drupal 7 des pages callback, à ça près qu'ils ont accès au container, et permettent d'utiliser la POO au mieux.

<?php

namespace Poney\Controller;

use Drupal\Core\Session\AccountInterface;
use Drupal\Core\StringTranslation\StringTranslationTrait;

use MakinaCorpus\Drupal\Sf\Controller;
use Drupal\node\NodeInterface;

class PoneyController extends Controller
{
    use StringTranslationTrait;

    /**
     * @return AccountInterface
     */
    private function getCurrentUser()
    {
        return $this->get('current_user');
    }

    /**
     * List of poneys.
     */
    public function listPoneysAction()
    {
        $list = [];
        $poneys = $this->get('database')
                       ->select('node', 'n')
                       ->fields('n', ['nid', 'title'])
                       ->condition('type', 'poney')
                       ->exectute()
                       ->fetchAllKeyed();
        foreach($poneys as $nid => $name) {
            $list[] = $this->t("Poney #@nid: @name", ['@nid' => $nid, '@name' => $name]));
        }
    
        $render_array['list'] = [
          '#theme' => 'item_list',
          '#items' => $list,
          '#title' => $this->t('Poneys'),
        ];
        return $render_array;
    }

    /**
     * Screen that displays poney details.
     */
    public function viewPoneyAction(NodeInterface $node)
    {
        if ($node->getType() !== 'poney') {
            throw $this->createNotFoundException(); // Same as throwing Symfony NotFoundHttpException
        }
        if ($node->uid !== $this->getCurrentUser()->uid) {
            throw $this->createAccessDeniedException(); // Same as throwing Symfony AccessDeniedHttpException
        }
    
        return [
            '#theme'     => 'node',
            '#node'      => $node,
            '#view_mode' => 'etable',
        ];
    }
}

La totalité des services n'ayant pas été backportés, il est préféréable du'iliser des requêtes simples.
il nous reste à déclarer nos routes, toujours dans le `hook_menu()` pour le moment :

<?php

use Poney\Controller\PoneyController;

/**
 * Implements hook_menu().
 */
function poney_menu() {
  $items = [];

  $items['poneys'] = [
    'title'             => 'List of poneys',
    'page callback'     => 'sf_dic_page',
    'page arguments'    => [PoneyController::class . '::listPoneys'],
    'type'              => MENU_CALLBACK,
  ];
  $items['poney/%node'] = [
    'title'             => 'Poney info',
    'page callback'     => 'sf_dic_page',
    'page arguments'    => [PoneyController::class . '::viewPoney', 1],
    'type'              => MENU_CALLBACK,
  ];

  return $items;
}

On remarquera que l'on passe la méthode en argument et non en callback, ce afin que sf_dic puisse travailler.


Création d'un formulaire simple

<?php

namespace Poney\Form;

use Drupal\node\NodeInterface;
use Drupal\Core\Entity\EntityManager;
use Drupal\Core\Form\FormBase;
use Drupal\Core\Form\FormStateInterface;

use Symfony\Component\DependencyInjection\ContainerInterface;

class PoneyEditForm extends FormBase
{
    static public function create(ContainerInterface $container)
    {
        // You can get the services you need as arguments here
        return new self(
            $container->get('entity.manager')
        );
    }

    private $entityManager;

    public function getFormId()
    {
        return 'poney_edit_form';
    }

    public function __construct(EntityManager $entityManager)
    {
        $this->entityManager = $entityManager;
    }

    public function buildForm(array $form, FormStateInterface $formState, NodeInterface $poney = null)
    {
        if (!$poney) {
            return $form;
        }

        $formState->setTemporaryValue('poney', $poney);

        $form['name'] = [
            '#title'            => t("Poney name"),
            '#type'             => 'textfield',
            '#attributes'       => ['placeholder' => $this->t('Caramel')],
            '#default_value'    => $poney->title,
            '#required'         => true,
        ];

        $form['actions']['#type'] = 'actions';
        $form['actions']['submit'] = [
            '#type'   => 'submit',
            '#value'  => $this->t("Submit"),
        ];

        return $form;
    }

    public function submitForm(array &$form, FormStateInterface $formState)
    {
        /* @var $poney NodeInterface */
        $poney = $formState->getTemporaryValue('poney');
        $poney->title = $formState->getValue('title');
        $this->entityManager->getStorage()->save($poney);
    }
}

et la route associée :

<?php

/**
 * Implements hook_menu().
 */
function poney_menu() {
  $items['poney/%node/edit'] = [
    'title'            => "Poney management",
    'page callback'    => 'sf_dic_page_form',
    'page arguments'   => ['Poney\Form\PoneyEditForm', 1],
    'access callback'  => TRUE,
    'type'             => MENU_CALLBACK,
  ];
  return $items;
}

Actualités en lien

Image
Logo Symfony Makina Corpus
09/11/2021

Comment démarrer un projet Symfony 5 en 5 minutes ?

Depuis quelques versions, le framework Symfony fournit de nombreux outils pour bâtir très rapidement une application fonctionnelle. Voyons ce qu'on peut faire en 5 minutes.

Voir l'article
Image
Drupal 9
04/11/2021

Migration de Drupal 8 à Drupal 9

Retour d'expérience sur la montée de version de plusieurs sites de Drupal 8 à Drupal 9.

Voir l'article
30/12/2020

Varnish et Drupal 9 : le vidage de cache ciblé

La mise en place d'un cache de pages anonymes Varnish devant un Drupal 9 permet une mise en place relativement aisée d'un vidage automatique des pages mises en cache en se basant sur la politique de tags de Drupal. Cet article devrait vous donner les bases pour commencer à comprendre et expérimenter le système.

Voir l'article

Inscription à la newsletter

Nous vous avons convaincus