Refonte

Suite au parcours compliqué de correction des bugs à répétition, nous choisissons de partir de zéro sur une architecture de module respectant le modèle MVC, qui utilisera les fonctionnalités de services disponibles par le noyau MMI V3.

On souhaite expliquer ici comment on va structurer les dossiers, comment on va séparer la logique métier de l'affichage, et comment on va utiliser les fonctionnalités puissantes du noyau MMI (comme l'uploader de fichiers natif).

Structure Standard d'un Module

Chaque nouveau module Todd doit respecter cette arborescence stricte pour garantir la lisibilité et la maintenabilité du code :

plugins/todd/modules/todd_nomdumodule/
├── admin.php      # [CONTROLLER/VIEW] L'interface du formulaire de création/édition
├── handler.php    # [MODEL/CONTROLLER] Traitement des données, interactions avec la BDD, requêtes AJAX
├── view.php       # [VIEW] Le rendu final public du module (HTML)
├── install.php    # trqnsfert des éléments nécessaires au fonctionnement du module à la BDD
└── assets/        # [ASSETS] Les ressources statiques
    ├── css/
    │   ├── admin.css
    │   └── view.css
    └── js/
        ├── admin.js
        └── view.js

Principes MVC appliqués :

  • Ne placez jamais de balise <style> ou <script> en dur dans view.php ou admin.php. Elles doivent être isolées dans le dossier assets/ et chargées via la classe App::enqueueCSS() et App::enqueueJS().
  • Ne placez jamais de requêtes SQL ($db->...) dans les fichiers de vue (view.php ou admin.php). Toute la logique métier, la préparation des variables et les interactions en base de données doivent se dérouler dans handler.php.

Modèle & Logique : handler.php

Le fichier handler.php est le cœur du traitement. Il est inclus à différents moments du cycle de vie par le système parent. C'est ici que l'on lit les données pour la vue, et c'est ici qu'on gère l'enregistrement depuis le panel admin.

Les requêtes SQL ne doivent s'exécuter que dans ce fichier !

Exemple d'architecture

<?php
// plugins/todd/modules/todd_example/handler.php

// ---------------------------------------------------------
// SECTION A : HOOKS (Crochets système)
// ---------------------------------------------------------

/**
 * Hook `saveContent`
 * Appelé silencieusement par le système quand l'admin clique sur "Enregistrer".
 * Permet d'altérer le tableau $payload avant sa sérialisation JSON en BDD.
 */
if (isset($module_action) && $module_action == 'saveContent') {
    // Si on a des traitements de textes spécifiques, conversions, etc.
    if (isset($_POST['example_title'])) {
        $payload['example_title'] = strip_tags(trim($_POST['example_title']));
    }
}

// ---------------------------------------------------------
// SECTION B : PRÉPARATION DE LA VUE (Lecture BDD)
// ---------------------------------------------------------

/**
 * Cette section est exécutée lorsqu'on a besoin d'afficher la vue (view.php).
 * C'est ici que l'on manipule la globale $payload pour s'assurer
 * que les variables ont des valeurs par défaut exploitables par le HTML.
 */
if (isset($payload)) {
    // Fournir des valeurs par défaut sécurisées à la vue
    $title = isset($payload['example_title']) ? $payload['example_title'] : 'Titre par défaut';
    $logo_url = isset($payload['example_logo_url']) ? $payload['example_logo_url'] : '';
    $message = isset($payload['example_message']) ? $payload['example_message'] : '';

    // Exemple d'un appel SQL (strictement réservé à ce fichier)
    // $items = $db->getArray("SELECT * FROM {DBPFX}votre_table WHERE parent_id = :id", [':id' => $contentId]);
}

// ---------------------------------------------------------
// SECTION C : POINTS D'ENTRÉE AJAX (WebServices)
// ---------------------------------------------------------

/**
 * Lorsque le client JS appelle `plugins/todd/scripts/todd.ws.php`
 * avec l'action 'moduleAction' et le paramètre 'module_action=getCustomData'.
 */
if (isset($module_action) && $module_action == 'getCustomData') {
    // Réaliser ici des traitements complexes sans bloquer le rendu de la page
    $result['status'] = 1;
    $result['data'] = ['message' => 'Hello from DB'];
}

Utilisation de l'Uploader Natif du Noyau MMI : uploader.ws.php

On nee ré-écrit JAMAIS de script d'upload PHP (move_uploaded_file, vérification MIME, etc). IL existe dans le noyau MMI, un gestionnaire déjà prêt et sécurisé : scripts/core/uploader.ws.php.

Que fait cet uploader ?

  • Il vérifie la sécurité des fichiers (extensions, failles MIME).
  • Il redimensionne automatiquement les images trop lourdes.
  • Il génère les miniatures (tailles xs, s, m, l).
  • Il place les fichiers dans le bon dossier (ex: uploads/images/).
  • Il inscrit le fichier dans la base de données globale {DBPFX}medias et retourne l'ID de ce média, assurant ainsi l'intégrité de la bibliothèque de médias du site.

Comment est-il utilisé (admin.js) ?

Imaginons qu'on doive téléverser un fichier image via un formulaire dans lequel on va passer une URL. On doit uploader le fichier en AJAX via Javascript dès que l'utilisateur le sélectionne dans ce formulaire d'édition.

/* plugins/todd/modules/todd_example/assets/js/admin.js */

// Capter le changement sur un input type=file classique HTML
$("#input_example_logo").on("change", function () {
  var fileInput = this;
  if (fileInput.files.length === 0) return;

  var formData = new FormData();
  // Le nom de la clé doit correspondre au champ PHP
  formData.append("file_upload", fileInput.files[0]);
  // Préciser le type (image, video, file, media) pour que l'uploader sache où le ranger
  formData.append("type", "image");

  $.ajax({
    url: SITE_URL + "scripts/core/uploader.ws.php",
    type: "POST",
    data: formData,
    contentType: false,
    processData: false,
    success: function (response) {
      var res = JSON.parse(response);
      if (res.status === 1) {
        // Succès de l'upload !
        // L'image est sur le serveur. On mémorise son URL dans notre champ caché Payload.
        $("#hidden_payload_logo_url").val(res.url).trigger("change");

        // Mettre à jour l'aperçu en direct:
        $("#preview_logo_img").attr("src", res.url);
      } else {
        alert("Erreur: " + res.msg);
      }
    },
  });
});

Le fichier admin.php (Interface d'édition)

Le fichier admin.php est le formulaire Vue-Modèle affiché dans le panel d'administration Todd.

  1. Listez tous vos champs de paramétrage.
  2. Tout champ HTML possédant l'attribut name="payload[votre_cle]" sera automatiquement sauvegardé dans le JSON global par le système parent.
  3. Chargez vos Javascript/CSS pour orchestrer une Preview en direct.
<?php
// plugins/todd/modules/todd_example/admin.php
// Chargement des Assets
App::enqueueCSS('todd_example_admin', SITE_URL . 'plugins/todd/modules/todd_example/assets/css/admin.css');
App::enqueueJS('todd_example_admin', SITE_URL . 'plugins/todd/modules/todd_example/assets/js/admin.js');
?>

<div class="row">
    <!-- Colonne Formulaire (Généralement 4 colonnes) -->
    <div class="col-md-4">
        <label>Titre</label>
        <input type="text" class="form-control" name="payload[example_title]" id="input_title" value="<?php echo isset($payload['example_title']) ? htmlspecialchars($payload['example_title']) : ''; ?>">

        <label>Logo</label>
        <input type="file" class="form-control" id="input_logo">
        <!-- Champ caché stockant l'Url post-upload -->
        <input type="hidden" name="payload[example_logo_url]" id="payload_logo_url" value="<?php echo isset($payload['example_logo_url']) ? htmlspecialchars($payload['example_logo_url']) : ''; ?>">
    </div>

    <!-- Colonne Aperçu en Direct (Preview) -->
    <div class="col-md-8">
        <div class="example-preview-box">
             <img id="preview_logo" src="<?php echo isset($payload['example_logo_url']) ? htmlspecialchars($payload['example_logo_url']) : ''; ?>" alt="Logo">
             <h2 id="preview_title"></h2>
        </div>
    </div>
</div>

Avec un court script dans assets/js/admin.js :

$(document).ready(function () {
  // Actualisation du titre en live
  $("#input_title")
    .on("input", function () {
      $("#preview_title").text($(this).val());
    })
    .trigger("input"); // Initialiser au démarrage
});

Le fichier view.php (Vue Publique)

Le fichier view.php est extrêmement simple : il ne contient que du HTML et des balises sémantiques. Aucune requête SQL n'y figure. Les variables (ex: $title, $logo_url) ont déjà été préparées et nettoyées plus tôt par le fichier handler.php en SECTION B.

<?php
// plugins/todd/modules/todd_example/view.php
$module_head_content = "
<link rel='stylesheet' href='".SITE_URL."plugins/todd/modules/todd_example/assets/css/view.css'>
";
?>

<div class="todd-example-container" style="background-color: <?php echo htmlspecialchars($bg_color); ?>;">
    <?php if (!empty($logo_url)): ?>
        <img src="<?php echo htmlspecialchars($logo_url); ?>" class="todd-example-logo" alt="Logo">
    <?php endif; ?>

    <h1 class="todd-example-title"><?php echo htmlspecialchars($title); ?></h1>
    <p class="todd-example-message"><?php echo nl2br(htmlspecialchars($message)); ?></p>
</div>

Préparation des styles

Comme todd_view/index.php n'appelle pas le moteur de rendu de la page racine et App::enqueueCSS() n'est pas chargé car il s'agit d'un affichage pleine page isolé. On utilise alors $module_head_content pour injecter vos feuilles de styles en public complet.

Dans todd/index.view, on utilise la fonction PHP ob_start() qui permet de mettre du matériel en cache et de le charger au bon moment et au bon endroit. Lorsque la fonction ob_start() est appelée, elle ouvre un tampon (buffer) dans lequel toutes les sorties PHP vont être redirigées. Ensuite, on va les récupérer au moment voulu dans une variable avec par exemple la fonction ob_get_clean().

Dans un fichier view.php du module.

<?php
    // ...
    ob_start();
?>
<style>
    <?php echo $fonts; ?>
    <?php echo $style_attribute; ?>
</style>
<link rel="stylesheet" href="<?php echo SITE_URL; ?>plugins/todd/modules/todd_information/assets/css/view.css">
<?php
$module_head_content = ob_get_clean();
?>

et dans le fichier todd_view/index.html où sera intégré la vue de ce module :

<?php
    if (isset($module_head_content))
    echo (string)$module_head_content;
?>

On peut ainsi récupérer les valeurs de certaines variables qui ont été définies ailleurs dynamiquement et que nous n'aurions pas pu récupérer autrement.


Installation : install.php

Lors de l'installation de l'extension TODD, le processus va appeler tous les fichiers install.php présents avec chaque module. C'est dans ce fichier au'on déclare les opérations à faire sur la base de données essentiellement :

  • création d'une entrée au nom du module dans la base todd_modules si elle n'existe pas déjà ;
  • créations de tables spécifiques au module si besoin (par exemple si le module doit sauvegarder des données le concernant aui peuvent changer dans le temps) ;
  • donner les droits nécessaires d'écriture et de suppression sur les tables précitées ;
  • on attribue à l'occasion des valeurs aux propriétés label (nom affiché du module) et icon (icône affichée à côté du nom) quel que soit le module. Les propriétés techniaues is_active et requires_moderation sont renseignées aussi.
<?php
// plugins/todd/modules/todd_information/install.php

// This is executed when the Todd administrator clicks "Refresh Modules" or installs the module manually.
global $db;

$module_code = 'todd_information';

$check = $db->get1x1("SELECT id FROM {DBPFX}todd_modules WHERE code = :code", [':code' => $module_code]);

if (!$check) {
    $maxOrder = $db->get1x1("SELECT MAX(sort_order) FROM {DBPFX}todd_modules") + 10;

    $sql = "INSERT INTO {DBPFX}todd_modules (code, label, icon, sort_order, is_active, requires_moderation)
            VALUES (:code, 'Informations (Nouveau MVC)', 'icon-info', :order, 1, 0)";

    $db->dml($sql, [':code' => $module_code, ':order' => $maxOrder]);

//    echo "Module $module_code installé avec succès.\n";
} else {
//    echo "Module $module_code déjà installé.\n";
}

Utilisation des composants Limitless

L'équipe de Tiria a fait le choix d'utiliser la librairie de composants Limitless. Le cœur du noyau MMI est responsable de charger cette librairie. L'utilisation d'un composant spécifiaue s'opère en deux parties : une déclaration de variables et un appel de fonctions (par exemple pour initialiser le composant) dans un fichier JS puis son utilisation dans le fichier PHP/HTML.

Voici un exemple d'utilisation avec un composant de sélection de couleurs (color-picker)

Partie JS

On appelle la fonction dans admin.js avec éventuellement des variables définies au préalable. On l'associe à l'élément via sa classe.

// Color palette
const demoPalette = [
  ["#000", "#444", "#666", "#999", "#ccc", "#eee", "#f3f3f3", "#fff"],
  ["#f00", "#f90", "#ff0", "#0f0", "#0ff", "#00f", "#90f", "#f0f"],
  ["#f4cccc", "#fce5cd", "#fff2cc", "#d9ead3", "#d0e0e3", "#cfe2f3", "#d9d2e9", "#ead1dc"],
  ["#ea9999", "#f9cb9c", "#ffe599", "#b6d7a8", "#a2c4c9", "#9fc5e8", "#b4a7d6", "#d5a6bd"],
  ["#e06666", "#f6b26b", "#ffd966", "#93c47d", "#76a5af", "#6fa8dc", "#8e7cc3", "#c27ba0"],
  ["#c00", "#e69138", "#f1c232", "#6aa84f", "#45818e", "#3d85c6", "#674ea7", "#a64d79"],
  ["#900", "#b45f06", "#bf9000", "#38761d", "#134f5c", "#0b5394", "#351c75", "#741b47"],
  ["#600", "#783f04", "#7f6000", "#274e13", "#0c343d", "#073763", "#20124d", "#4c1130"],
];
$(".colorpicker-palette").spectrum({
  showPalette: true,
  showInput: true,
  preferredFormat: "hex",
  palette: demoPalette,
  appendTo: "#modal_edit_content",
});

Partie PHP/HTML

On intègre le composant dans le HTML :

            <div class="col-md-4 form-group">
                <label>Couleur texte</label>
                <input type="text" class="form-control colorpicker-palette" name="info_text_color" id="input_info_text_color" value="<?php echo htmlspecialchars($textColor); ?>">
            </div>