Skip to Content
MoodlePartie 8 — Les thèmes Moodle : le guide completSurcharges : templates Mustache, renderers PHP, layouts

Chapitre 8.3 — Surcharges : templates Mustache, renderers PHP, layouts

Partie 8 : Les thèmes — Chapitre 3/5 Public : dev front. On dépasse le SCSS : ici on change le HTML et la logique de rendu, sans toucher au core. Version de référence : Moodle 5.2 (Boost, Mustache, theme_overridden_renderer_factory).

⏱️ TL;DR

  • Trois niveaux de surcharge, du moins au plus intrusif : templates Mustache (le HTML), renderers PHP (la logique qui prépare le HTML), layouts (la structure de page).
  • Surcharger un template : placer theme/maboite/templates/<component>/<nom>.mustache → il gagne sur celui du plugin/core (résolution par convention, ch. 7.2 §5). Ordre : thème courant → thèmes parents → composant.
  • Surcharger un renderer : theme_overridden_renderer_factory (dans config.php) + une classe \theme_maboite\output\core_renderer (ou celle d’un plugin) qui redéfinit une méthode.
  • Layouts : config.php mappe chaque type de page (standard, course, login, admin…) à un fichier layout/*.php, qui assemble le HTML de haut niveau (header, drawers, régions de blocs, footer) via un template Mustache.
  • Icon map : un thème peut remapper les icônes (FontAwesome 6) via classes/output/icon_system_fontawesome.
  • JS dans un thème : un thème peut avoir des modules AMD (amd/src/) comme n’importe quel plugin.
  • Règle d’or : surcharger le moins possible, le plus ciblé possible — chaque surcharge est une dette à maintenir à chaque upgrade (ch. 8.4).

🎯 Objectifs

  • Surcharger un template Mustache de n’importe quel composant, avec le bon ordre de résolution.
  • Surcharger un renderer PHP (core ou plugin) via la fabrique.
  • Écrire un layout custom (drawers, régions de blocs).
  • Remapper des icônes et ajouter du JS dans un thème.
  • Mesurer le coût de maintenance de chaque type de surcharge.

1. Les trois leviers, par intrusivité croissante

Le SCSS (ch. 8.2) change l’apparence. Quand il ne suffit pas — il faut changer le markup ou la logique — on a trois leviers, du plus sûr au plus risqué :

Plus on va à droite, plus on touche à des fondations partagées et plus la dette de maintenance augmente (le core évolue sous vos surcharges). D’où la règle : rester le plus à gauche possible.

💡 Pour un dev React : c’est une échelle « override de style → override de composant de présentation → override de composant conteneur → override de layout ». Vous restez dans une logique de shadowing : ne remplacer que ce qu’il faut, au niveau le plus fin, pour minimiser la surface qui diverge du framework.


2. Surcharger un template Mustache

Le plus courant et le moins risqué. Rappel de la résolution (ch. 7.2 §5) : pour render_from_template('core/user_menu'), Moodle cherche dans l’ordre :

  1. theme/maboite/templates/core/user_menu.mustache (thème courant) → gagne
  2. theme/boost/templates/core/user_menu.mustache (thème parent)
  3. <component>/templates/user_menu.mustache (le composant, ici lib/templates/)

Procédure : copiez le template original dans votre thème, au même chemin relatif, puis modifiez-le.

# Copier le template de Boost (ou du core) dans le thème mkdir -p public/theme/maboite/templates/core cp public/theme/boost/templates/core/loginform.mustache \ public/theme/maboite/templates/core/loginform.mustache # … puis éditer la copie.

Exemple : ajouter un texte d’accueil sur la page de connexion, dans la copie de loginform.mustache :

{{! theme/maboite/templates/core/loginform.mustache — extrait modifié }} <div class="login-welcome mb-4 text-center"> <h1 class="h3">{{#str}}welcometo, theme_maboite, MaBoîte{{/str}}</h1> <p class="text-muted">{{#str}}logintagline, theme_maboite{{/str}}</p> </div> {{! …le reste du template original, inchangé… }}

⚠️ Piège n°1 (le plus coûteux) : une surcharge de template est une copie figée à un instant T. Si le core fait évoluer loginform.mustache (nouveau champ, correctif d’accessibilité, changement de contexte) dans une version ultérieure, votre copie ne le reçoit pas — vous gardez l’ancien markup, parfois cassé. À chaque upgrade de Moodle, il faut diff vos templates surchargés contre les originaux et reporter les changements. Ne surchargez que ce que vous devez, et notez chaque surcharge.

⚠️ Piège n°2 : la surcharge doit respecter le même contexte (mêmes variables {{ }}) que l’original — c’est le renderer/PHP qui fournit ce contexte, pas vous. Retirer une variable attendue ou en inventer une non fournie casse le rendu.


3. Surcharger un renderer PHP

Parfois le HTML est produit par un renderer (une méthode PHP), et le template ne suffit pas : il faut changer la logique qui prépare les données, ou intercepter une méthode de rendu du core. On active la fabrique de renderers surchargés (déjà mise dans config.php, ch. 8.2) :

$THEME->rendererfactory = 'theme_overridden_renderer_factory';

Puis on crée une classe qui étend le renderer cible et redéfinit une méthode.

3.1 Surcharger le core_renderer

<?php // public/theme/maboite/classes/output/core_renderer.php namespace theme_maboite\output; defined('MOODLE_INTERNAL') || die(); class core_renderer extends \core_renderer { /** * Surcharge le rendu du pied de page pour y injecter une mention MaBoîte. */ public function standard_footer_html(): string { $output = parent::standard_footer_html(); $output .= \html_writer::div( get_string('poweredby', 'theme_maboite'), 'maboite-footer-credit text-center small text-muted' ); return $output; } }

La convention : \theme_<nom>\output\core_renderer extends \core_renderer. La fabrique détecte cette classe et l’utilise à la place du core_renderer standard. On appelle parent:: pour réutiliser le comportement d’origine et n’ajouter que le delta.

3.2 Surcharger le renderer d’un plugin

Même principe, en visant le renderer du plugin :

<?php // public/theme/maboite/classes/output/mod_simplecheck/renderer.php namespace theme_maboite\output\mod_simplecheck; defined('MOODLE_INTERNAL') || die(); class renderer extends \mod_simplecheck\output\renderer { // Redéfinir render_checklist(), par exemple, pour un markup différent. }

⚠️ Piège : surcharger un renderer est plus intrusif qu’un template. Vous dépendez de la signature et du comportement interne de la méthode du core/plugin, qui peuvent changer d’une version à l’autre sans template à diff. Réservez-le aux cas où le template ne suffit vraiment pas (logique de préparation à modifier). Préférez, quand c’est possible, surcharger le template que la méthode rend.

💡 Pour un dev React : surcharger un renderer, c’est remplacer le composant conteneur (celui qui calcule les props) et pas seulement le composant de présentation (le template). Plus de pouvoir, mais vous vous couplez à l’implémentation interne — comme forker un composant de lib au lieu de lui passer un renderItem. À n’utiliser que si l’API de présentation ne vous laisse pas faire.


4. Les layouts : la structure de page

Un layout est le HTML de haut niveau d’une page : <header>, drawers, régions de blocs, <main>, <footer>. Moodle choisit un layout selon le type de page (pagelayout). Boost fournit des layouts (columns2, login, admin, secure…) que votre thème hérite. Pour en surcharger un :

4.1 Déclarer le layout dans config.php

// public/theme/maboite/config.php (extrait) $THEME->layouts = [ // On surcharge SEULEMENT le layout « login », les autres restent hérités de Boost. 'login' => [ 'file' => 'login.php', 'regions' => [], ], // Exemple de layout standard avec une région de blocs à droite. 'standard' => [ 'file' => 'columns2.php', 'regions' => ['side-pre'], 'defaultregion' => 'side-pre', ], ];

4.2 Le fichier de layout

<?php // public/theme/maboite/layout/login.php defined('MOODLE_INTERNAL') || die(); // Le layout prépare un contexte et rend un template Mustache de layout. $templatecontext = [ 'sitename' => format_string($SITE->fullname), 'output' => $OUTPUT, // le renderer, exposé au template 'bodyattributes' => $OUTPUT->body_attributes(), 'logourl' => $OUTPUT->get_logo_url(), ]; echo $OUTPUT->render_from_template('theme_maboite/login', $templatecontext);

4.3 Le template de layout

{{! theme/maboite/templates/login.mustache }} <!DOCTYPE html> <html {{{ output.htmlattributes }}}> <head> {{{ output.standard_head_html }}} <title>{{{ output.page_title }}}</title> </head> <body {{{ bodyattributes }}}> {{{ output.standard_top_of_body_html }}} <div id="page" class="maboite-login-page"> <main id="region-main"> <img src="{{logourl}}" alt="{{sitename}}" class="maboite-login-logo mb-4"> {{{ output.main_content }}} </main> </div> {{{ output.standard_end_of_body_html }}} </body> </html>

Un layout appelle des méthodes du renderer (standard_head_html, main_content, standard_end_of_body_html…) qui injectent le contenu obligatoire (CSS, JS, contenu de la page). C’est la « coquille » ; le contenu vient du core.

⚠️ Piège critique : un layout doit appeler standard_head_html, standard_top_of_body_html, standard_end_of_body_html et main_content. Oublier l’un d’eux casse la page (pas de CSS/JS chargé, contenu manquant, débogage invisible). Partez toujours d’une copie du layout de Boost et modifiez le minimum — n’écrivez jamais un layout de zéro sans ces appels.

💡 Pour un dev React : un layout Moodle ≈ votre layout.tsx racine (le <html><head><body> + slots). output.main_content est le {children}, standard_head_html injecte les balises <head> (comme un <Head>/metadata), standard_end_of_body_html charge les scripts. Vous surchargez la coquille en réutilisant ces « portails » du framework.


5. Icon map : remapper les icônes

Moodle 5.2 utilise FontAwesome 6. Un thème peut remapper les icônes (changer quelle icône FA correspond à quelle clé Moodle) via une classe :

<?php // public/theme/maboite/classes/output/icon_system_fontawesome.php namespace theme_maboite\output; defined('MOODLE_INTERNAL') || die(); class icon_system_fontawesome extends \core\output\icon_system_fontawesome { public function get_core_icon_map(): array { $iconmap = parent::get_core_icon_map(); // Remplacer l'icône « edit » par une autre variante FA. $iconmap['core:t/edit'] = 'fa-solid fa-pen-to-square'; return $iconmap; } }

Utile pour une identité visuelle cohérente ou pour brancher un jeu d’icônes différent, sans toucher au core ni aux plugins.


6. Ajouter du JavaScript dans un thème

Un thème est un plugin : il peut avoir des modules AMD (theme/maboite/amd/src/*.js, ch. 7.3), chargés via js_call_amd depuis un layout ou un renderer, ou déclarés pour toutes les pages via un hook. Cas d’usage : une bascule de menu maison, un comportement de header au scroll, l’initialisation d’un composant de marque.

// dans un layout ou renderer du thème $PAGE->requires->js_call_amd('theme_maboite/header', 'init');

⚠️ Piège : ne chargez pas de JS lourd sur toutes les pages depuis le thème (ch. 7.6 §7) — cela dégrade les Core Web Vitals de tout le site. Chargez au plus près de l’usage, et privilégiez le CSS/SCSS quand un effet peut se faire sans JS (transitions, :hover, position: sticky…).


7. Choisir le bon niveau de surcharge

La hiérarchie de décision : SCSS d’abord (le moins cher), template si le markup doit changer, renderer seulement si la logique de préparation l’exige, layout pour la structure de page. Chaque cran vers la droite augmente la dette que vous paierez à chaque upgrade de Moodle. Le chapitre 8.4 chiffre honnêtement cette dette.


✏️ Exercices

Exercice 1 — Ordre de résolution. Trois fichiers existent : theme/maboite/templates/core/loginform.mustache, theme/boost/templates/core/loginform.mustache, lib/templates/loginform.mustache. Lequel est utilisé quand le thème actif est maboite (enfant de boost) ?

✅ Solution

theme/maboite/templates/core/loginform.mustache (le thème courant gagne). L’ordre de résolution est : thème courant → thème(s) parent(s) (boost) → composant (lib/). Le premier trouvé l’emporte. Si le fichier du thème maboite n’existait pas, Boost serait utilisé ; sinon le core.

Exercice 2 — Template ou renderer ? Vous voulez (a) ajouter une ligne de texte dans le footer ; (b) changer quelles données apparaissent dans le menu utilisateur (retirer une entrée selon une règle métier). Quel levier pour chacun ?

✅ Solution

(a) Template (ou SCSS si c’est purement visuel) : ajouter du markup statique dans une copie du template du footer, ou une méthode de renderer simple qui appelle parent:: + ajout (comme standard_footer_html). (b) Renderer : filtrer les entrées relève de la logique de préparation du contexte, pas du markup — on surcharge la méthode PHP qui construit le menu (en respectant l’API). Règle : markup → template ; quelles données / logique → renderer.

Exercice 3 — Layout cassé. Après avoir écrit un layout login.php custom, la page de connexion s’affiche sans style et le débogage n’apparaît plus. Cause probable ?

✅ Solution

Le layout (ou son template) n’appelle pas les méthodes obligatoires du renderer : standard_head_html (charge le CSS/JS et les métadonnées) et/ou standard_end_of_body_html (charge les scripts de fin et affiche le débogage). Sans standard_head_html, aucun CSS n’est injecté → page nue. Correctif : repartir d’une copie du layout de Boost et ne modifier que le nécessaire, en conservant standard_head_html, standard_top_of_body_html, main_content et standard_end_of_body_html.

Exercice 4 — Dette d’upgrade. Un thème surcharge 25 templates du core. À l’upgrade 5.2 → 5.3, quel travail cela implique-t-il et comment le limiter ?

✅ Solution

Il faut, pour chacun des 25 templates, diff la version 5.3 du template original contre la 5.2 (celle qu’on a copiée) et reporter les changements (nouveaux champs de contexte, correctifs d’accessibilité, restructurations) dans la copie surchargée — sinon on garde un markup obsolète, parfois cassé. Pour limiter : (1) surcharger le moins possible (préférer SCSS quand ça suffit) ; (2) documenter chaque surcharge et sa raison ; (3) surcharger de façon ciblée (n’inclure que le bloc modifié quand c’est possible via des partials) ; (4) automatiser le diff en CI. C’est le cœur du coût des thèmes (ch. 8.4).

Exercice 5 — JS dans le thème. Vous voulez un header qui se réduit au scroll. Faut-il du JS, et comment l’ajouter proprement (ou l’éviter) ?

✅ Solution

Souvent évitable en CSS : position: sticky + une transition de hauteur, voire des animations liées au scroll en CSS moderne — préférable (pas de JS, pas de coût perf). Si l’effet exige du JS (mesurer le scroll, ajouter/retirer une classe), créer un module AMD dans theme/maboite/amd/src/header.js et l’appeler via $PAGE->requires->js_call_amd('theme_maboite/header', 'init')pas globalement sur toutes les pages si inutile. Règle : CSS d’abord, JS seulement si nécessaire, et chargé au plus près de l’usage.


🧠 Quiz de révision

Q1. Quels sont les trois leviers de surcharge au-delà du SCSS, par intrusivité croissante ?

Réponse

Templates Mustache (le HTML), renderers PHP (la logique qui prépare le HTML), layouts (la structure de page). Plus on va vers la droite, plus la dette de maintenance augmente.

Q2. Comment surcharge-t-on un template, et quel est l’ordre de résolution ?

Réponse

En plaçant theme/<theme>/templates/<component>/<nom>.mustache (même chemin relatif que l’original). Résolution : thème courant → thème(s) parent(s)composant. Le premier trouvé gagne. La surcharge doit respecter le même contexte.

Q3. Qu’active theme_overridden_renderer_factory et comment surcharge-t-on le core_renderer ?

Réponse

Elle permet au thème de remplacer des renderers. On crée \theme_<nom>\output\core_renderer extends \core_renderer et on redéfinit une méthode (en appelant parent:: pour ne changer que le delta). Même principe pour un renderer de plugin (\theme_<nom>\output\<plugin>\renderer).

Q4. Que doit impérativement contenir un layout, sous peine de casser la page ?

Réponse

Les appels aux méthodes obligatoires du renderer : standard_head_html (CSS/JS/métadonnées), standard_top_of_body_html, main_content (le contenu de la page) et standard_end_of_body_html (scripts de fin, débogage). Les oublier casse le style et/ou le contenu.

Q5. Quelle est la règle d’or des surcharges et pourquoi ?

Réponse

Surcharger le moins possible et le plus ciblé possible, en restant au niveau le moins intrusif (SCSS > template > renderer > layout). Parce que chaque surcharge est une copie figée qui diverge du core à chaque upgrade et qu’il faut re-diffuser/re-tester deux fois par an. Moins de surcharges = moins de dette.

Chapitre suivant : 04 — Est-on vraiment flexible ? La réponse honnête — la démonstration argumentée : ce qui est facile (couleurs, fontes, header/footer, login), moyen (markup des activités core), dur ou risqué (formulaires moodleform, plugins tiers sans templates, maintenir des dizaines de surcharges). Thème from scratch sans Boost : possible mais déconseillé, et pourquoi. Études de cas de thèmes très transformés, multi-tenant, dark mode, design tokens.