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
21/03/2024

L’ano­ny­mi­sa­tion sous stéroïdes avec le DBTools­Bundle

Le DbTools­Bundle permet d’ano­ny­mi­ser des tables d’un million de lignes en seule­ment quelques secondes. Cet article vous présente la métho­do­lo­gie mise en place pour arri­ver à ce résul­tat.

Voir l'article
Image
Encart article DrupalCamp 2024
06/03/2024

Makina Corpus, parte­naire de la Drupal­Camp 2024

Nous sommes fiers d’an­non­cer que Makina Corpus est le spon­sor de la Drupal­Camp à Rennes. Notre expert vous y propose une confé­rence « migrer de Drupal 7 à Drupal 10 ».

Voir l'article
Image
Encart article 2 : Itéra­tions vers le DDD et la clean archi­tec­ture avec Symfony
20/02/2024

Itéra­tions vers le DDD et la clean archi­tec­ture avec Symfony (2/2)

Quels virages avons-nous pris après un premier projet expé­ri­men­tal pour stabi­li­ser notre concep­tion logi­cielle, et que ferons-nous plus tard ?

Voir l'article

Inscription à la newsletter

Nous vous avons convaincus