Makina Blog

Le blog Makina-corpus

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

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
Encart blog DBToolsBundle
18/07/2024

DbTools­Bundle : sortie de la version 1.2

Décou­vrez les nouveau­tés de cette nouvelle version ainsi que les fonc­tion­na­li­tés à venir de la prochaine version majeure.

Voir l'article
Image
Symfony
11/04/2024

Access Control, biblio­thèque PHP pour gérer des droits d’ac­cès

Suite à un projet de gestion métier opéra­tion­nel dont la durée de vie et la main­te­nance sont à long termes, nous avons expé­ri­menté un passage de celui-ci sur l’archi­tec­ture hexa­go­nale et la clean archi­tec­ture.

Voir l'article
Image
Encart D7 vers Drupal 11
04/04/2024

Migration d'un site Drupal 7 en Drupal 11

Trucs, astuces et "bouts" de code pour migrer votre site web de Drupal 7 à Drupal 11. Compte-rendu d'une conférence donnée au Drupalcamp Rennes 2024.

Voir l'article

Inscription à la newsletter

Nous vous avons convaincus