11. Les modules

openADS dispose d’un système de modules, qui permet d’enrichir l’application avec du code externe à celle-ci.

Note

À ne pas confondre avec les paramètres d’URL module qui indiquent si la requête HTTP concerne un formulaire, un sous-formulaire, un onglet, etc.

11.1. Fonctionnellement

Un module pourra enrichir l’application de 3 manières différentes:

  • ajouter des « boutons » (action de portlet) qui déclencheront un traitement avec un message ou afficherons des informations sur une nouvelle page ou dans une fenêtre modale;
  • modifier l’apparence et les interactions d’une interface/page en ajoutant des scripts CSS ou JS;
  • déclencher des traitements à certains moments, soit pour modifier une interface (en modifiant un formulaire par exemple) soit pour modifier/stocker des informations calculées (ou récupérées depuis un service tiers par exemple).

Les modules sont associés à des objets précis (par exemple instruction numéro 12) ou bien à une classe d’objet entière (par exemple toutes les instructions).

Cette association a lieu lorsqu’un administrateur de l’application va sur la page de paramétrage d’un objet (par exemple dans le menu Paramétrage dossiers > Workflows > Événement) et va dans le nouvel onglet Modules, et ajoute un module: le module sera alors associé à cet évènement (objet précis).

Actuellement il n’y a aucun moyen pour un administrateur d’associer un module à une classe d’objet, cela se fait plutôt au niveau de l’installation du module.

Lors de l’association d’un module à un objet, il est possible de renseigner différentes informations le concernant ainsi que cette association:

  • Nom: le module qui sera associé, choisi dans la liste de tous les modules
  • Déclencheur (optionnel): un moment de déclenchement du module (notamment pour les modules de traitements)
  • Ordre (optionnel): un ordre de déclenchement dans le cas d’un module de traitement ou bien un ordre d’affichage dans le cas où le module ajoute un/des « boutons »
  • Paramètres (optionnel): des éléments de paramétrage du module (au format INI) pour personaliser le comportement du module (pour cette association)

Il est possible d’ajouter plusieurs fois le même module à un objet précis, si ses paramètres sont différents.

11.2. Techniquement

Un module correspond à un script PHP déposé dans un dossier du même nom dans le dossier modules à la racine de l’application.

Par exemple un module « helloworld » correspondra à un script PHP nommé helloworld.php (snake case) dans le dossier openads/modules/helloworld/.

Au niveau du code PHP, le module correspond à une classe du même nom que le module (snake case), dans un espace de noms du même nom que le module (camel case).

Pour reprendre l’exemple du module helloworld, cela donnera une classe helloworld dans l’espace de noms Module\Helloworld\.

Cette classe héritera de la classe abstraite des modules nommée module définie dans le fichier openads/obj/module.class.php.

Dans cette classe abstraite sont définies 3 méthodes qu’il faudra surcharger impérativement:

  • get_description: pour spécifier un libellé décrivant le module (simple phrase/slogan)
  • install: si le module a besoin de réaliser des opérations une seule fois pour être mis en oeuvre
  • uninstall: pour déconstruire ce qui aurait été éventuellement réalisé lors de la phase d’installation

Toujours dans cette classe abstraite on trouve 5 méthodes particulières qui permettent d’enrichir l’application. Ces méthodes correspondent au 2 premiers points de la section Fonctionnellement:

  • get_actions: pour ajouter des « boutons » à l’interface
  • get_header_scripts_js et get_header_styles_css: pour ajouter des scripts JS et CSS dans la balise <head> HTML
  • get_inline_scripts_js et get_inline_styles_css: pour ajouter des scripts JS et CSS dans le code de la page HTML

11.2.1. Les « hooks »

Enfin un mécanisme spécifique a été implémenté pour permettre aux modules d’intervenir quasiment à n’importe quel moment/endroit du code, via un système de « hooks ».

Le « hooking » ne correspond pas à un « patron de conception » bien nommé, mais se rapproche du principe fonctionnel d’un « decorator » implémenté avec un système d”« event dispatcher » au lieu d’un « wrapper », ou autrement d’un « template method » dont le contenu de l’implémentation des « hook methods » est déporté dans une autre classe (les modules).

11.2.1.1. La convention de nommage

A l’instar de l’exemple dans cette réponse, des « callbacks » (ou « hooks ») sont positionnés dans un grand nombre d’endroits du code de l’application, pour permettre une exécution de code tiers presque n’importe quand/où.

Par contre au lieu d’un système d’enregistrement de « callbacks » (ou « hooks »), il a été choisi d’utiliser une convention de nommage.

Le format est le suivant: <classe de l'objet>_<nom du hook>.

Celle-ci fonctionne de la manière suivante: lors du déclenchement du traitement du « callback » (ou « hook »), le gestionnaire de module va parcourir tous les modules associés à l’objet courant (ou à la classe de l’objet courant) et pour chacun d’eux, s’il possède une méthode dont le nom correspond au nom du « hook » en cours, alors la méthode du module va être appelée.

Par exemple, lors de l’ajout d’une instruction, au moment de la vérification des données dans la méthode instruction::verifier, puisqu’on a un « hook » défini au début de cette méthode qui s’appelle verifier_pre, alors si un module a été associé à la classe instruction et que ce module possède la méthode instruction_verifier_pre et bien elle va être appelée par le gestionnaire de module.

Cette convention de nommage permet d’économiser une étape où les modules enregistrent/déclarent leurs « callbacks » (ou « hooks »).

On utilisera le terme « hook » dans la suite de ce document plutôt que « callback ».

11.2.1.2. La méthode main

En plus de cette convention de nommage il existe un second système pour que les modules puissent réagir aux « hooks » et il s’agit de la méthode main.

Un module peut déclarer le nom d’une méthode main via la méthode get_main_method, et s’il le fait, alors à chaque déclenchement d’un « hook » c’est cette méthode qui sera appelée (sauf s’il existe une méthode qui répond à la convention de nommage décrite ci-dessus pour ce « hook »).

Cela permet de mutualiser du code qui serait utile à plusieurs « hook » mais surtout pour les débutants cela permet de découvrir les « hooks » disponibles en loggant les appels à cette méthode, puisqu’elle sera appelée à chaque « hook ».

11.2.1.3. Le contexte passé aux méthodes des modules

Lors de l’appel d’une méthode d’un module associée à un « hook », celle-ci recevra en argument un « contexte » sous la forme d’un tableau qui peut contenir certaines données intéressantes disponibles au moment du déclenchement du « hook » dans la méthode parente.

Par exemple si dans la méthode instruction::verifier l’application exécute une requête SQL pour récupérer des éléments qui est stockée dans la variable $elements, et bien le « hook » nommé verifier_post en fin de méthode passera cette variable dans le tableau de contexte, afin que le module qui implémente la méthode instruction_verifier_post puisse consulter et manipuler ces $elements (si besoin).

11.2.1.4. Le gestionnaire de module « dispatch » les « hooks »

Les modules et les « hooks » sont gérés par une classe nommée module_manager dans openads/obj/module_manager.class.php.

Celle-ci défini une méthode run_hooks qui sert à déclarer/déclencher un « hook » lorsqu’elle est appelée dans un ojet métier, par exemple dans le code de l”instruction on trouve:

...
class instruction extends instruction_gen {
   ...
   function set_form_default_values(&$form, $maj, $validation) {

      // préparation du contexte [NDLR: commentaire ajouté]
      $data = array('form' => &$form, 'maj' => &$maj, 'validation' => &$validation);

      // lancement des méthodes de modules pour le « hook » nommé 'set_form_default_values_pre' [NDLR: commentaire ajouté]
      $this->f->module_manager->run_hooks('set_form_default_values_pre', $this, $data);
      ...
      ...
      // lancement des méthodes de modules pour le « hook » nommé 'set_form_default_values_post' [NDLR: commentaire ajouté]
      $this->f->module_manager->run_hooks('set_form_default_values_post', $this, $data);
   }
   ...
}

Lorsque la méthode run_hooks est appelée, elle reçoit en paramètre un nom de « hook », (dans l’exemple set_form_default_values_pre et set_form_default_values_post), ainsi que l’objet métier courant (dans l’exemple $this), et un contexte (dans l’exemple la variable $data).

Cette méthode va alors récupérer tous les modules de l’objet métier spécifié (dans l’exemple c’est instruction, et supposons que son identifiant est 12).

Cette récupération des modules se déroule comme suit:

  1. récupération de tous les modules associés à la classe de l’objet métier (donc tous les modules associés uniquement à la classe instruction (cf ci-dessous)

  2. récupération de tous les modules associés à l’objet métier précis (donc tous les modules associés à l’objet instruction dont l’identifiant est 12, cf ci-dessous)

  3. récupération de tous les modules associés aux objets liés à l’objet métier précis (ici les différents objets métiers liés à l”instruction). Imaginons que l”instruction 12 ait l”évènement associé numéro 30, alors cela récupèrerait tous les modules associés à l’objet précis d”évènement 30 (cf ci-dessous) et de même pour chacun des objets liés à l”instruction donc dossier, action, etat, avis_decision, signataire_arrete, document_numerise, autorite_competente, etc.

    Note

    Le gestionnaire de modules va charger tous les modules des objets liés à un objet métier précis mais sans charger les modules des classes des objets liés, ni les modules de leurs objets liés (les modules des objets liés des objets liés) pour éviter un phénomène de « cascade » ou de « boucle infinie ».

  4. uniquement dans le cadre de l”ajout d’une instruction, cela va aussi récupérer les modules des évènements possibles pour le dossier d’instruction courant. C’est un cas particulier.

Note

Avant de récupérer les modules d’un objet métier (ou d’une classe d’objet) le gestionnaire de module va d’abord vérifier si le système des modules est activés pour cet objet (cf l’activation des modules pour un objet métier)

Après avoir récupéré (et instancié et stocké dans le registre des modules chargés) tous les modules nécessaires, la méthode run_hook continue en déterminant le nom de la méthode que les modules doivent avoir implémenté. C’est la convention de nommage qui préfixe le nom de la méthode par la classe de l’objet métier (donc instruction) et qui ajoute ensuite le nom du « hook » (ici set_form_default_values_pre, ce qui donnera la méthode instruction_set_form_default_values_pre).

Chaque module est ensuite testé pour voir s’il implémente (ou non) la méthode avec le nom défini par la convention de nommage (c’est à dire: <classe de l'objet>_<nom du hook>). Si un module implémente cette méthode, alors celle-ci est appelé en lui passant en paramètre le contexte (variable $data) et cela est fait pour chaque module implémentant cette méthode (par ordre décroissant de la valeur ordre du module avec les valeurs vides en dernier).

11.2.1.4.1. Activation/Désactivation du système des modules pour un objet métier donné

Le système des modules est désactivé par défaut pour tous les objets métiers de l’application, afin d’éviter de recherche « sans arrêts » des modules qui ne seront jamais associés à aucun objet métier (cf L’association entre un module et un objet ou un type d’objet), et ainsi éviter des ralentissements inutiles de l’application.

Pour activer les modules pour un objet métier donné, il faut s’assurer que le fichier dyn/modules.inc.php existe et contienne une variable nommée $modules_enabled_for qui soit un tableau contenant le nom de la classe de l’objet métier en question.

Par exemple, pour activer les modules sur les dossiers d’instruction, il faut que la variable $modules_enabled_for contiennent la valeur dossier_instruction:

$modules_enabled_for = array(
   ...
   'dossier_instruction'
   ...
);

11.2.2. L’association entre un module et un objet ou un type d’objet

11.2.2.1. La table lien_module

Comme indiqué plus haut dans la section Fonctionnellement, les modules sont associés à un objet précis ou bien à une classe d’objet (aussi appelé un type d’objet ou un nom d’objet). Techniquement, cela correspond à une ligne dans la nouvelle table lien_module (dont la classe PHP correspondante est dans le fichier openads/obj/lien_module.class.php).

Cette table reprend les informations indiquées dans la section Fonctionnellement, et comprends également les informations concernant l’objet métier. Cela donne les colonnes suivantes:

  • lien_module: Identifiant technique
  • object_name: Objet (classe) auquel est associé le module
  • object_id: Objet (instance) auquel est associé le module
  • module: Nom du module
  • declencheur: Moment de déclenchement (uniquement pour les traitements)
  • ordre: Ordre de priorité d’exécution ou de positionnement pour les portlets
  • parametres: Paramètres du module (au format INI)

Exemple d’association:

  • un module avec une instruction précise (numéro 12): object_name=instruction et object_id=12
  • un module avec toutes les instructions: object_name=instruction et object_id=NULL

11.3. Configuration des modules

La configuration du système des modules s’effectue dans le fichier dyn/modules.inc.php.

Celui-ci doit contenir obligatoirement 3 variables:

  • $modules_dir: le chemin vers le répertoire contenant les dossiers des modules
  • $hooks: la liste des libellés métier des « hooks » disponibles dans l’application
  • $modules_enabled_for: la liste des objets métiers pour lesquels les modules sont activés (cf l’activation des modules pour un objet métier)

11.4. Exemples

Avec l’application sont joints 3 modules d’exemple pour mieux appréhender les fonctionnalités du système de modules:

  • helloworld: ajoute un bouton « Hello XXX » où XXX est défini via le paramétrage du module, et lorsqu’on clic sur le bouton cela affiche une fenêtre modale présentant un objet (également paramétré au niveau du module)
  • tweakinstru: modifie le type du champ date_evenement lors de l’ajout d’une instruction pour qu’il soit saisissable si l’évènement spécifié correspond à celui associé au module
  • addfield: ajoute un champ « dynamique » à l’objet associé, et permet d’enregistrer des données dans ce champ, de les vérifier, et également de l’afficher dans les listings et les recherches

11.5. Tutoriel

Imaginons qu’on veuille modifier l’affichage du champ date_evenement d’une instruction (lorsqu’elle est en mode consultation) pour que cette date soit toujours au 1er jour du mois de la date réelle et que la date entière soit affichée en rouge.

Voici les étapes génériques habituelles à poursuivre:

  1. se demander à quel objet métier ce module va devoir être associé, ici on veut modifier une instruction, donc nos options sont les suivantes:

    • associer le module à un objet précis (une seule instruction)
    • associer le module à une classe d’ojbet (toutes les instructions)
    • associer le module à un objet précis (un seul évènement), c’est un cas particulier des instructions qui utilisent également les modules de l”evenement qui leur est lié.

    mais actuellement dans openADS, seul les modules d”évènements sont implémentés au niveau de l’interface d’administration, donc on va choisir la 3ème option.

  2. déterminer si le module a besoin d’ajouter du CSS ou du JS (si oui il faudra implémenter les méthodes get_header_scripts_js / get_header_styles_css et/ou get_inline_scripts_js / get_inline_styles_css)

  3. déterminer si le module a besoin d’implémenter une méthode de « hook » et laquelle ou lesquelles (si un « hook » n’est pas présent alors il faudra faire une demande pour qu’il soit ajouté dans l’application) pour effectuer un traitement à un moment donné ou bien modifier les valeurs ou l’affichage d’un objet

  4. déterminer quel sera l’objet métier traité par ce module (la plupart du temps l’objet métier est le même que celui de l’objet associé au module, mais dans le cas particulier des modules d”évènements qui servent également aux instructions, le module à beau être associé à un évènement, l’objet métier traité sera une instruction), car cela détermine le préfixe des méthodes de « hook »

  5. déterminer quel sera le contexte (argument $data) reçu par les différentes méthodes de « hook » implémentées par le module, et notamment quelles sont les données disponibles dans chacun de ces contextes (si un élément de contexte est manquant pour les besoins de ce module, faire une demande pour qu’il soit ajouté dans l’application)

Dans notre cas d’exemple voici les réponses:

  1. le module sera associé à un objet précis evenement et c’est l”administrateur de l’application qui pourra réaliser cette association dans le menu Paramétrage dossiers > Workflows > Événement dans le nouvel onglet Modules en ajoutant notre module
  2. le module aura besoin d’ajouter du CSS pour modifier la couleur du champ, donc on utilisera la méthode get_inline_styles_css car ce sera dans le cadre d’un sousformulaire
  3. le module devra implémenter la méthode correspondant au « hook » set_form_default_values_post pour pouvoir modifier la valeur de la date (lorsqu’on est en mode consultation)
  4. le module traitera un objet métier du type instruction, car bien qu’il soit associé à un évènement, il sera instancié dans le cadre de l’affichage d’une instruction (les instructions utilisent les modules de l”évènement qui leur est associé)
  5. le contexte du « hook » donne accès aux éléments suivants: 'form' => $form, 'maj' => $maj, 'validation' => $validation, et la variable $data['form'] sera suffisante pour modifier le formulaire affiché de l”instruction (car même en mode consultation le framework openmairie affiche un formulaire) et la variable $data['maj'] nous donnera le « mode » actuel ('3' pour la consultation)

Y’a plus qu’à :-)

On va appeller le module tuto_date_evt, donc on créer un répertoire openads/modules/tuto_date_evt et un fichier PHP openads/modules/tuto_date_evt/tuto_date_evt.php.

Dans ce dernier on déclare la classe tuto_date_evt qui hérite de la classe module:

<?php

namespace Module\TutoDateEvt;

require_once __DIR__.'/../../obj/module.class.php';

use \Module\module;
use \om_dbform;

class tuto_date_evt extends module {
}

Puis on implémente les 3 méthodes abstraites obligatoires:

public function get_description() {
    return __("Modifie la date d'évènement de l'instruction");
}

public function install() {
    return true;
}

public function uninstall() {
    return true;
}

Ensuite on sait qu’on va devoir ajouter du CSS pour modifier l’apparence du champ date_evenement, donc on ajoute la méthode adéquate:

public function get_inline_styles_css($position) {
    if ($position != 'bottom') {
        return array();
    }
    return array($this->get_url_assets_path_base().'/'.$this->get_short_name().'.css');
}

Et on va créer ce fichier CSS dans openads/modules/tuto_date_evt/tuto_date_evt.css avec le code suivant:

#date_evenement { color: red; }

On poursuit côté PHP en revenant à notre module pour ajouter la méthode associé au « hook » set_form_default_values_post comme suit:

public function instruction_set_form_default_values_post (array &$data) {
    // (optionnel) si le context ne contient pas la clé 'form' dont on a besoin, on log l'info et on arrête le traitement
    if (! isset($data['form']) || empty($data['form'])) {
        $this->log(__METHOD__, "le contexte ne contient pas la clé 'form' ou celle-ci est vide (BUG?!)");
        return;
    }
    // (optionnel) idem pour la clé 'maj'
    if (! isset($data['maj']) || empty($data['maj'])) {
        $this->log(__METHOD__, "le contexte ne contient pas la clé 'maj' ou celle-ci est vide (BUG?!)");
        return;
    }
    // on souahite uniquement modifier la valeur lorsqu'on est en consultation, donc dans le cas 'maj' = '3'
    if ($data['maj'] !== '3') {
        return;
    }
    // (optionnel) on vérifie que l'objet métier a bien été associé
    if (empty($this->object)) {
        $this->log(__METHOD__, "l'objet métier n'a pas été associé au module (BUG?!)");
        return;
    }
    // (optionnel) on vérifie que l'objet métier associé est du bon type
    if (($obj_type = get_class($this->object)) !== 'instruction') {
        $this->log(__METHOD__, "l'objet métier associé au module n'est pas du type 'instruction' mais '$obj_type' (BUG?!)");
        return;
    }

    // ici on sait que l'objet métier est bien défini et que c'est une instruction
    $instruction = $this->object;

    // on récupère la valeur actuelle de la date d'évènement
    $date_evt_current = $instruction->getVal('date_evenement');
    // nous aurions aussi pu utiliser, mais c'est moins orienté objet et moins sûr
    // $date_evt_current = $data['form']->val['date_evenement'];

    // on détermine la nouvelle valeur de la date d'évènement
    $date_evt_new = '01'.substr($date_evt_current, 2);

    // on enregistre la valeur dans le formulaire
    $data['form']->setVal('date_evenement', $date_evt_new);
}

On s’assure que les modules sont activés dans dyn/modules.inc.php pour les objets instruction et evenement (cf l’activation des modules pour un objet métier).

$modules_enabled_for = array(
    'instruction',
    'evenement'
);

Voilà, le module est fini d’être codé, et un administrateur peut l’associer à un évènement via le menu Paramétrage dossiers > Workflows > Événement dans le nouvel onglet Modules en ajoutant le module.

Ensuite il faut aller sur l’affichage d’une instruction dont l”évènement correspond à celui paramétré ci-dessus, et constater que le champ date_evenement est affiché en rouge avec en valeur le 1er jour du mois.

En cas de problème, il est possible d’activer le mode DEBUG en ajoutant un fichier à la racine de l’application nommée log.level contenant DEBUG, soit l’équivalent de la commande suivante: echo DEBUG > openads/log.level.