Chapitre 01 — Vue d’ensemble : cycle de vie d’une requête HTTP dans Moodle
Objectif du chapitre : comprendre comment Moodle traite une requête HTTP de bout en bout, pourquoi son architecture ne ressemble à aucun framework moderne que vous connaissez, et acquérir le vocabulaire fondamental (globals, frankenstyle, autoloading, plugins) sans lequel aucun code Moodle n’est lisible.
Mini-sommaire
- Moodle n’est PAS un framework MVC classique
- Cycle de vie d’une requête
- Les globals sacrés
- Frankenstyle : la convention de nommage universelle
- Autoloading : le dossier
classes/ - « Tout est plugin »
- Structure d’une page type
- Component library & outils du développeur
- Résumé — points clés
1. Moodle n’est PAS un framework MVC classique
1.1 Un peu d’histoire (et pourquoi elle compte encore)
Si vous arrivez de l’écosystème Next.js/React, votre premier réflexe en ouvrant le code source de Moodle sera de chercher le routeur, les contrôleurs, le point d’entrée unique. Vous ne les trouverez pas — ou plutôt, vous en trouverez des fragments récents greffés sur un organisme beaucoup plus ancien. Pour comprendre Moodle, il faut accepter un fait : Moodle est né en 2002, écrit par Martin Dougiamas en PHP procédural, à une époque où PHP 4 régnait, où les namespaces n’existaient pas, où Composer n’existait pas, et où « une page web = un fichier PHP » était le modèle mental dominant.
Vingt-quatre ans plus tard, Moodle fait tourner des centaines de millions de comptes utilisateurs dans le monde. Cette longévité a un prix : la rétrocompatibilité est une valeur cardinale du projet. Des dizaines de milliers de plugins tiers existent, des milliers d’institutions ont du code maison. Moodle HQ ne casse donc jamais l’architecture d’un coup ; il la fait évoluer par strates successives. Le résultat est un code base géologique : on y lit les époques comme des couches sédimentaires. Du procédural 2002 dans lib/moodlelib.php, de l’orienté objet 2010 dans les renderers, des namespaces 2013 dans les dossiers classes/, de l’injection de dépendances PSR-11 depuis 2024 (\core\di, Moodle 4.4), un routeur moderne depuis Moodle 4.5/5.0 (\core\router).
C’est déroutant au début, puis cela devient une force : quand vous savez dater une API, vous savez quel style de code adopter et quelle doc lire.
1.2 L’architecture réelle : « page scripts + APIs transverses »
Le modèle architectural de Moodle n’est pas MVC. C’est ce qu’on peut appeler « page scripts + APIs transverses » :
- Les page scripts : chaque URL correspond (encore très majoritairement) à un fichier PHP physique.
https://monsite.fr/mod/quiz/view.php?id=42exécute littéralement le fichiermod/quiz/view.php.https://monsite.fr/course/view.php?id=3exécutecourse/view.php. Le fichier joue à la fois le rôle de route, de contrôleur et (dans le code ancien) de vue. Il n’y a pas de front controller unique qui dispatcherait tout. - Les APIs transverses : le « core » de Moodle fournit une soixantaine d’APIs horizontales que chaque page script consomme : l’API base de données (
$DB), l’API de sortie ($OUTPUT, renderers, templates Mustache), l’API de fichiers, l’API d’accès (capabilities), l’API de formulaires (moodleform), l’API d’événements, l’API de tâches planifiées, etc. C’est là que réside la vraie valeur du framework : le page script est un mince orchestrateur, les APIs font le travail lourd.
Autrement dit : Moodle est un framework par ses bibliothèques, pas par sa structure de contrôle. Symfony ou Laravel imposent comment votre code est appelé ; Moodle impose surtout quelles APIs vous appelez.
💡 Pour un dev React : le modèle « un fichier = une URL » est exactement le pages router de Next.js (
pages/mod/quiz/view.tsx→/mod/quiz/view)… mais sans le framework autour. Pas degetServerSideProps, pas de layout automatique : chaque fichier doit lui-même charger la configuration, vérifier l’authentification et produire le HTML complet. Pensez à un projet Next.js pages router où chaque page commencerait par appeler manuellement le bootstrap de l’application. C’est verbeux, mais totalement prévisible : quand vous voyez une URL Moodle, vous savez immédiatement quel fichier ouvrir dans votre éditeur. Cette « débuggabilité » par correspondance URL ↔ fichier est un vrai confort au quotidien.
1.3 L’exception qui grandit : \core\router
Depuis Moodle 4.5, un vrai routeur HTTP existe : \core\router, construit au-dessus du micro-framework Slim 4 et de composants PSR (PSR-7 pour les requêtes/réponses, PSR-15 pour les middlewares, PSR-11 pour le container). Les routes sont déclarées par attributs PHP 8 sur des classes contrôleurs :
// Extrait simplifié du style des contrôleurs routés modernes.
namespace core_course\route\controller;
use core\router\route;
use core\router\schema\parameters\path_parameter;
use Psr\Http\Message\ResponseInterface;
class course_management {
#[route(
path: '/course/{course}',
pathtypes: [
new path_parameter(name: 'course', type: \core\param::INT),
],
)]
public function view_course(int $course): ResponseInterface {
// Logique de la page, retourne une réponse PSR-7.
// ...
}
}En Moodle 5.x, seule une minorité de pages est passée sur ce routeur (certaines pages de gestion de cours, les nouveaux web services REST…). La migration est progressive et s’étalera sur des années. Les URLs routées passent par r.php (un shim à la racine web) ou par des règles de réécriture du serveur si l’administrateur les a configurées. Retenez donc : le routeur existe, il représente l’avenir, mais le présent — et 95 % du code que vous lirez — reste le page script. Ce chapitre décrit donc d’abord le modèle classique, celui que vous rencontrerez partout.
📚 Aller plus loin : la documentation du routeur est sur moodledev.io/docs/apis/subsystems/routing , et le projet de migration global sur moodledev.io/general/projects .
1.4 Comparaison honnête
| Aspect | Next.js (App/Pages router) | Framework PHP moderne (Symfony) | Moodle 5.2 |
|---|---|---|---|
| Routing | Basé fichiers, géré par le framework | Routeur central + attributs | Fichiers physiques (majoritaire) + \core\router (émergent) |
| Point d’entrée | Serveur Node unique | public/index.php (front controller) | Chaque page script (+ r.php pour les routes) |
| Bootstrap | Automatique | Kernel | require config.php en tête de chaque page |
| État partagé | Contexte React, stores | Container DI | Variables globales ($DB, $USER…) + \core\di depuis 4.4 |
| Vues | JSX/composants | Twig | Templates Mustache + renderers PHP |
| Modules | Packages npm | Bundles | Plugins « frankenstyle » |
| Typage | TypeScript | PHP 8 typé | PHP 8.3 typé dans le code récent, PHPDoc dans l’ancien |
L’honnêteté commande de le dire : Moodle est moins élégant qu’un framework né en 2020. Mais il est remarquablement documenté, remarquablement stable dans ses contrats d’API, et son modèle de plugins est l’un des plus aboutis du monde PHP. C’est ce qui rend l’investissement rentable.
2. Cycle de vie d’une requête
2.1 Vue d’ensemble : du navigateur au HTML
Suivons une requête GET https://elearning.exemple.fr/mod/quiz/view.php?id=42 :
- Le navigateur envoie la requête HTTP au serveur web (Apache ou Nginx + PHP-FPM).
- Le serveur web fait correspondre l’URL à un fichier du document root. Depuis Moodle 5.1, ce document root est le dossier
public/de l’installation (voir § 2.5). Le fichierpublic/mod/quiz/view.phpexiste : PHP l’exécute. Pas de réécriture d’URL, pas de dispatch — le système de fichiers est la table de routage. - Le page script commence — c’est une règle absolue — par charger la configuration :
require(__DIR__ . '/../../config.php');config.phpdéfinit l’objet global$CFG(identifiants de base de données, chemins, URL du site) puis, à sa dernière ligne, inclutlib/setup.php.lib/setup.phpest le véritable bootstrap : il charge le cœur du framework, initialise l’autoloader, ouvre la connexion à la base de données, démarre la session, restaure l’utilisateur courant dans$USER, prépare$PAGEet$OUTPUT. Quandsetup.phprend la main, l’environnement complet est disponible.- Le page script reprend : il lit ses paramètres (
required_param), vérifie l’authentification (require_login) et les permissions (require_capability), configure la page ($PAGE->set_url()…), puis produit le HTML :echo $OUTPUT->header();… contenu …echo $OUTPUT->footer();. - La réponse part vers le navigateur. Fin de la requête ; PHP étant shared-nothing, tout l’état en mémoire est détruit (seuls survivent la base de données, la session et les caches).
💡 Pour un dev React : le point 7 est capital si vous venez de Node. En PHP classique, chaque requête repart de zéro : pas de serveur qui reste en mémoire entre deux requêtes, pas de singleton qui survit, pas de fuite d’état possible entre deux utilisateurs. C’est comme si votre serveur Next.js redémarrait à chaque requête. Conséquence : le bootstrap (
config.php+setup.php) s’exécute à chaque page vue — d’où l’importance énorme des caches (MUC, opcache) que nous verrons plus tard.
2.2 config.php décortiqué
config.php est l’unique fichier de configuration de l’instance — l’équivalent de votre .env + next.config.js réunis, mais en PHP exécutable. Il est créé par l’installeur et vit à la racine de l’installation. Le voici, complet et commenté (par convention, tout fichier Moodle commence par l’en-tête de licence GPL v3+ ; nous l’omettons dans ce cours pour la lisibilité, mais il est obligatoire dans du vrai code) :
<?php
// config.php — configuration de l'instance Moodle.
// Ce fichier est le SEUL qui diffère entre vos environnements
// (dev, staging, prod). Il ne doit JAMAIS être versionné avec
// des identifiants réels.
unset($CFG); // Par prudence : on repart d'un objet vierge.
global $CFG; // Déclare la portée globale (utile si ce fichier
// est inclus depuis une fonction, cas des tests).
$CFG = new stdClass();
//=========================================================================
// 1. BASE DE DONNÉES
//=========================================================================
$CFG->dbtype = 'mariadb'; // 'mysqli', 'mariadb', 'pgsql', 'sqlsrv', 'oci'
$CFG->dblibrary = 'native'; // Toujours 'native' de nos jours.
$CFG->dbhost = 'localhost'; // Hôte du serveur de BDD.
$CFG->dbname = 'moodle'; // Nom de la base.
$CFG->dbuser = 'moodleuser'; // Utilisateur BDD.
$CFG->dbpass = 'motdepasse'; // Mot de passe BDD.
$CFG->prefix = 'mdl_'; // Préfixe de TOUTES les tables : la table
// « user » s'appelle physiquement mdl_user.
// Permet plusieurs applis dans une même base.
$CFG->dboptions = [
'dbpersist' => false, // Connexions persistantes (à éviter).
'dbsocket' => false, // Socket Unix plutôt que TCP.
'dbport' => '', // Port non standard éventuel.
'dbcollation' => 'utf8mb4_unicode_ci',
];
//=========================================================================
// 2. CHEMINS ET URL
//=========================================================================
// L'URL publique complète du site, SANS slash final.
$CFG->wwwroot = 'https://elearning.exemple.fr';
// Le répertoire de DONNÉES : fichiers uploadés, caches, sessions, logs
// temporaires. Doit être accessible en écriture par PHP et se trouver
// HORS de l'arborescence servie par le serveur web.
$CFG->dataroot = '/var/moodledata';
// Nom du dossier d'administration (permet de renommer /admin si un
// pare-feu d'entreprise bloque ce mot — cas rare mais réel).
$CFG->admin = 'admin';
// Permissions des dossiers créés dans dataroot (notez le 0 initial : octal).
$CFG->directorypermissions = 02777;
//=========================================================================
// 3. RÉGLAGES FORCÉS (optionnels)
//=========================================================================
// Toute propriété de $CFG définie ICI devient prioritaire sur la valeur
// stockée en base (table mdl_config) et apparaît grisée/verrouillée dans
// l'interface d'administration. Idéal pour figer la config en prod.
$CFG->debug = 0; // En dev : E_ALL via la constante DEBUG_DEVELOPER.
$CFG->debugdisplay = 0; // Ne jamais afficher les erreurs en prod.
// $CFG->debug = (E_ALL | E_STRICT); // ← version dev
// $CFG->debugdisplay = 1; // ← version dev
//=========================================================================
// 4. BOOTSTRAP — TOUJOURS EN DERNIÈRE LIGNE
//=========================================================================
require_once(__DIR__ . '/lib/setup.php');
// Pas de balise PHP fermante « ?> » : convention Moodle (et PHP moderne)
// pour éviter d'envoyer accidentellement des espaces avant les en-têtes HTTP.Quelques remarques importantes :
$CFGest un simplestdClass: les propriétés sont dynamiques.config.phpen définit une douzaine ;setup.phpet la tableconfigen base en ajouteront des centaines (chaque réglage de l’interface d’administration devient une propriété de$CFG).- Le mécanisme des forced settings est un pattern d’exploitation à connaître : tout ce que vous écrivez dans
config.phpl’emporte sur la base de données. On y met typiquementdebug, les réglages de proxy,session_handler_class(Redis), les réglages SMTP… C’est votre équivalent des variables d’environnement de déploiement. - Il existe des dizaines de réglages avancés documentés dans le fichier
config-dist.phplivré avec Moodle : lisez-le au moins une fois, c’est une mine.
⚠️ Piège : le
require(config.php)doit être la toute première instruction de chaque page script, avant toutecho, avant tout espace, avant tout octet de sortie. Si le moindre caractère est émis avant, les en-têtes HTTP (cookies de session notamment) ne pourront plus être envoyés et vous obtiendrez le fameux « Headers already sent ». Corollaire : un BOM UTF-8 en début de fichier ou un espace après un?>dans un fichier inclus suffit à casser la connexion. C’est la raison de la convention « pas de?>final ».
⚠️ Piège :
$CFG->wwwrootsans slash final, et$CFG->datarootjamais à l’intérieur du docroot. Simoodledataest servi par le serveur web, n’importe qui peut télécharger les fichiers privés des utilisateurs et les clés de session : c’est la faille de configuration numéro 1 des installations amateur. Moodle refuse d’ailleurs de démarrer s’il détecte que dataroot est accessible par le web.
2.3 lib/setup.php : le bootstrap du framework
Quand config.php termine par require_once(__DIR__ . '/lib/setup.php'), la vraie machinerie se met en route. Il est très instructif de lire ce fichier ; en voici la séquence logique, simplifiée :
- Défenses et constantes : vérification de la version de PHP, définition de
MOODLE_INTERNAL(la constante que tous les fichiers de bibliothèque testent pour interdire l’accès direct par URL), calcul de$CFG->dirroot,$CFG->libdir,$CFG->root, réglage du fuseau horaire, des limites mémoire. - Chargement des bibliothèques historiques :
lib/classes/component.php(le cerveau des plugins et de l’autoloading, chargé en premier), puis les grandes libs procédurales —lib/setuplib.php,lib/moodlelib.php(fonctions générales :get_config(),required_param()…),lib/weblib.php(sortie web :s(),format_string(),moodle_url…),lib/datalib.php(accès données historique),lib/accesslib.php(permissions),lib/outputlib.php, etc. Des milliers de fonctions deviennent disponibles. - Enregistrement de l’autoloader :
core_component::register_autoloader()— à partir d’ici, toute classe respectant les conventions (§ 5) se charge à la demande. - Connexion à la base :
setup_DB()instancie le driver correspondant à$CFG->dbtypeet le place dans le global$DB. Les centaines de réglages de la tableconfigsont alors fusionnés dans$CFG(sans écraser les forced settings). - Session et utilisateur :
\core\session\manager::start()démarre (ou restaure) la session PHP, peuple$SESSIONet$USER. Si personne n’est connecté,$USERest un utilisateur « non connecté » (id 0) — jamaisnull. - Objets de page :
$PAGE(instance demoodle_page) et$OUTPUT(unbootstrap_renderertemporaire, remplacé par le vrai renderer du thème au premier usage) sont créés.$SITEet$COURSEsont initialisés au « cours site ». - Divers : initialisation des caches (MUC), des gestionnaires de hooks (API hooks, depuis Moodle 4.4), vérification du mode maintenance, etc.
À la fin de setup.php, le contrôle revient à la ligne 2 de votre page script, avec un environnement complet. Coût typique : quelques dizaines de millisecondes grâce à opcache.
2.4 Les trois racines : dirroot, dataroot, wwwroot
Trois notions de « racine » coexistent et il faut les distinguer une fois pour toutes :
| Variable | Nature | Exemple | Contient |
|---|---|---|---|
$CFG->wwwroot | URL publique | https://elearning.exemple.fr | Sert à construire tous les liens ; jamais un chemin disque. |
$CFG->dirroot | Chemin disque du code | /srv/moodle/public | Le code source PHP servi (page scripts, plugins). Lecture seule en prod. |
$CFG->dataroot | Chemin disque des données | /var/moodledata | Fichiers uploadés (filedir/), caches, sessions, temp. Écriture requise, hors web. |
$CFG->root (5.1+) | Chemin disque de l’installation | /srv/moodle | Le parent : public/, config.php, et le futur code non public. Lecture seule, calculé automatiquement. |
Ne jamais confondre : wwwroot sert à générer des URLs (new moodle_url('/mod/quiz/view.php', ['id' => 42])), dirroot sert aux require_once de fichiers legacy (require_once($CFG->dirroot . '/mod/quiz/locallib.php')), dataroot ne sert quasiment jamais directement (on passe par l’API Fichiers).
2.5 Nouveauté Moodle 5.1 : la racine web est public/
Historiquement (2002 → 5.0), le document root du serveur web était la racine même du dépôt Moodle : config.php, lib/, vendor/ (pour les devs), tout était potentiellement adressable par URL, et la sécurité reposait sur des gardes-fous (MOODLE_INTERNAL, fichiers .htaccess, extensions .php non exécutables directement…). Depuis Moodle 5.1, la quasi-totalité du code — y compris tous les plugins — a été déplacée dans un sous-dossier public/, et c’est ce dossier que le serveur web doit servir :
# Apache — vhost Moodle 5.1+
DocumentRoot /srv/moodle/public# Nginx — vhost Moodle 5.1+
root /srv/moodle/public;L’arborescence d’une installation 5.2 ressemble donc à :
/srv/moodle/ ← $CFG->root
├── config.php ← configuration (hors docroot !)
└── public/ ← $CFG->dirroot = DocumentRoot du serveur
├── index.php
├── r.php ← shim du routeur \core\router
├── admin/ …
├── course/view.php
├── lib/ ← le cœur (moodlelib.php, classes/, setup.php…)
├── mod/quiz/view.php ← les plugins vivent DANS public/
├── blocks/ … theme/ … local/ …
└── …Points essentiels pour vous :
$CFG->wwwrootet$CFG->dirrootne changent pas de sémantique :wwwrootreste l’URL du site,dirrootpointe désormais surpublic/(là où est le code) — le code existant continue de fonctionner sans modification dans la quasi-totalité des cas. Une nouvelle variable en lecture seule,$CFG->root, pointe sur la racine de l’installation.- La motivation est la sécurité : Moodle peut enfin posséder du code et des fichiers structurellement inaccessibles par le web (au lieu de compter sur des conventions). C’est le même mouvement que Symfony ou Laravel ont fait il y a quinze ans avec leur dossier
public/. - Pour une installation migrée de 5.0 vers 5.1+, la seule opération d’infrastructure est de repointer le DocumentRoot du vhost vers
public/. Les URLs publiques ne changent pas ;config.phpreste reconnu (Moodle sait le trouver à la racine de l’installation ou, par compatibilité, danspublic/). - Les plugins tiers s’installent désormais sous
public/:public/mod/monplugin,public/local/montool, etc. Les tutoriels antérieurs à 5.1 qui disent « copiez dans mod/ » restent vrais, à ce préfixe près.
📚 Aller plus loin : le guide officiel de la restructuration est sur moodledev.io/docs/5.1/guides/restructure et le projet de fond sur moodledev.io/general/projects/directoryrestructure . Dans la suite du cours, quand nous écrivons
mod/quiz/view.php, lisezpublic/mod/quiz/view.phpsur une installation 5.1+.
3. Les globals sacrés
Voici le sujet qui fait tousser tout développeur moderne : Moodle expose son état applicatif via des variables globales PHP. Huit d’entre elles sont omniprésentes ; les voici d’un coup d’œil avant de les détailler :
| Global | Classe | Rôle | Initialisé par |
|---|---|---|---|
$CFG | stdClass | Configuration du site (chemins, réglages) | config.php puis setup.php |
$DB | moodle_database (driver concret) | Accès base de données | setup_DB() |
$USER | stdClass (ligne de mdl_user enrichie) | Utilisateur courant | session |
$SESSION | stdClass | Données de session arbitraires | session |
$PAGE | moodle_page | Métadonnées et setup de la page en cours | setup.php |
$OUTPUT | core_renderer (du thème actif) | Production du HTML | setup.php / thème |
$COURSE | stdClass (ligne de mdl_course) | Cours courant | require_login() |
$SITE | stdClass | Le cours « site » (id = 1) | setup.php |
Règle de syntaxe PHP à graver dans le marbre : à l’intérieur d’une fonction ou d’une méthode, une variable globale n’est pas visible tant qu’on ne l’a pas importée avec le mot-clé global. Dans le corps principal d’un page script (portée globale), elles sont directement accessibles.
function local_monplugin_count_students(int $courseid): int {
global $DB; // ← OBLIGATOIRE, sinon $DB vaut null ici.
return $DB->count_records('user_enrolments', /* … */);
}⚠️ Piège : oublier
global $DB;en tête de fonction est LA première erreur de tout débutant Moodle. Le symptôme : « Call to a member function get_record() on null ». En portée globale (page script), ça marche ; dès que vous extrayez le code dans une fonction, ça casse. Prenez l’habitude d’ouvrir chaque fonction utilisant des globals par une ligneglobal $DB, $USER, $CFG;ne listant que ceux réellement utilisés. (Dans les méthodes de classes modernes, préférez\core\di— voir § 3.9.)
3.1 $CFG — la configuration
$CFG agrège la configuration « d’infrastructure » (écrite dans config.php) et la configuration « applicative » (table config, gérée dans l’interface d’admin). C’est un stdClass aux propriétés dynamiques :
global $CFG;
echo $CFG->wwwroot; // https://elearning.exemple.fr
echo $CFG->dirroot; // /srv/moodle/public
echo $CFG->release; // « 5.2 (Build: 2025xxxx) »
echo $CFG->theme; // « boost » — réglage venu de la table config.
// Les réglages PROPRES À UN PLUGIN ne sont PAS dans $CFG mais dans la
// table config_plugins ; on y accède par get_config() :
$apikey = get_config('local_monplugin', 'apikey');
set_config('apikey', 'sk-123', 'local_monplugin'); // écritureRetenez la nuance : $CFG->truc = config du core ; get_config('frankenstyle', 'truc') = config d’un plugin. Et rappelez-vous le § 2.2 : toute propriété écrite en dur dans config.php est forcée et non modifiable par l’interface.
3.2 $DB — la base de données
$DB est une instance d’une sous-classe de moodle_database (mariadb_native_moodle_database, pgsql_native_moodle_database…). Elle offre une API d’abstraction complète — le chapitre 02 lui est entièrement consacré, contentons-nous d’un aperçu :
global $DB;
// Lire un enregistrement (objet stdClass) — MUST_EXIST lève une exception
// moodle_exception si absent, plutôt que de retourner false.
$user = $DB->get_record('user', ['id' => 42], '*', MUST_EXIST);
// Lire plusieurs enregistrements.
$teachers = $DB->get_records('user', ['deleted' => 0], 'lastname ASC');
// SQL manuel : les noms de tables entre accolades — Moodle y substitue
// le préfixe (mdl_) — et TOUJOURS des paramètres liés, jamais de
// concaténation.
$sql = "SELECT u.id, u.firstname, u.lastname
FROM {user} u
JOIN {user_enrolments} ue ON ue.userid = u.id
WHERE ue.timecreated > :since";
$recent = $DB->get_records_sql($sql, ['since' => time() - DAYSECS]);
// Écrire.
$record = new stdClass();
$record->name = 'Mon enregistrement';
$record->timecreated = time();
$id = $DB->insert_record('local_monplugin_items', $record);⚠️ Piège : ne jamais écrire le préfixe en dur (
FROM mdl_user) ni interpoler une variable dans le SQL (WHERE id = $id). La première erreur casse toute installation dont le préfixe n’est pasmdl_; la seconde est une injection SQL. La syntaxe canonique estFROM {user}+ paramètres nommés:id. Les revues de code du répertoire officiel de plugins rejettent systématiquement ces deux fautes.
3.3 $USER — l’utilisateur courant
$USER contient l’enregistrement de l’utilisateur connecté (ligne de la table user, enrichie de préférences et de données de session d’authentification). Point crucial : il n’est jamais null. Un visiteur anonyme a un $USER->id de 0 ; le compte « invité » (guest) a un vrai id. D’où les fonctions de test dédiées :
global $USER;
if (isloggedin() && !isguestuser()) {
// Un « vrai » utilisateur authentifié.
echo 'Bonjour ' . fullname($USER); // fullname() respecte les formats
// de noms configurés et la langue.
} else {
// Anonyme OU invité : dans les deux cas, pas de données personnelles.
}
// Autres tests fréquents :
if (is_siteadmin()) { /* administrateur du site */ }
// Les préférences utilisateur ont leur API (ne modifiez pas $USER à la main) :
set_user_preference('local_monplugin_collapsed', 1);
$collapsed = get_user_preferences('local_monplugin_collapsed', 0);⚠️ Piège : ne testez jamais
if ($USER->id)seul pour savoir si quelqu’un est connecté : l’utilisateur invité a un id non nul et passerait le test. La paire canonique estisloggedin() && !isguestuser(). De même, ne comparez pas$USER->id == $someuseridpour décider d’un droit : passez par l’API des capabilities (chapitre sur la sécurité).
3.4 $SESSION — la session
$SESSION est un stdClass libre, persisté dans le stockage de session (fichiers de dataroot, base de données ou Redis selon la config), propre à chaque utilisateur connecté. On y range de petites données d’état de navigation :
global $SESSION;
// Mémoriser un état de navigation entre deux pages.
$SESSION->local_monplugin_lastfilter = 'active';
// Lecture défensive : la propriété peut ne pas exister.
$filter = $SESSION->local_monplugin_lastfilter ?? 'all';
unset($SESSION->local_monplugin_lastfilter); // Nettoyage.Convention : préfixez vos clés par votre frankenstyle pour éviter les collisions (vous partagez cet objet avec tout Moodle et tous les plugins). Et n’y stockez jamais rien de volumineux : la session est sérialisée/désérialisée à chaque requête.
3.5 $PAGE — la page en construction
$PAGE (classe moodle_page) est le descripteur de la page en cours : son URL canonique, son contexte de sécurité, son titre, son layout, ses fichiers JS/CSS, son fil d’Ariane. Chaque page script doit le configurer avant d’émettre le moindre HTML :
global $PAGE;
$PAGE->set_url(new moodle_url('/local/monplugin/view.php', ['id' => $id]));
$PAGE->set_context(context_system::instance());
$PAGE->set_title(get_string('pagetitle', 'local_monplugin'));
$PAGE->set_heading($SITE->fullname);
$PAGE->set_pagelayout('standard'); // 'standard', 'admin', 'incourse',
// 'popup', 'embedded'…
// Charger un module JavaScript AMD/ESM du plugin (chapitre JS) :
$PAGE->requires->js_call_amd('local_monplugin/main', 'init');Le chapitre 04 (Output API) détaillera tout cela ; retenez pour l’instant que $PAGE = configuration déclarative de la page, à faire avant $OUTPUT->header().
3.6 $OUTPUT — le renderer
$OUTPUT est l’instance du renderer principal du thème actif (core_renderer ou une sous-classe du thème). C’est lui qui sait produire le HTML « à la mode du thème » : en-tête complet de page, pied de page, boutons, notifications, icônes, et surtout le rendu des templates Mustache :
global $OUTPUT;
echo $OUTPUT->header(); // <!DOCTYPE html> … jusqu'au
// début de la zone de contenu.
echo $OUTPUT->heading('Mes éléments'); // <h2> stylé par le thème.
echo $OUTPUT->notification(get_string('saved'), 'success');
// Rendu d'un template Mustache avec un contexte de données :
echo $OUTPUT->render_from_template('local_monplugin/item_list', [
'items' => $items,
'canedit' => true,
]);
echo $OUTPUT->footer(); // Fin du HTML + JS différé.💡 Pour un dev React :
$PAGE+$OUTPUTforment le « layout system » de Moodle.$PAGE->set_pagelayout()≈ choisir un layout Next.js ;$OUTPUT->header()/footer()≈ le<Layout>qui enveloppe vos pages ;render_from_template()≈ rendre un composant avec ses props — sauf que Mustache est logic-less : toute la préparation des données se fait côté PHP, le template ne fait qu’afficher. Vous retrouverez cette philosophie « dumb components » familière.
3.7 $COURSE — le cours courant
$COURSE contient l’enregistrement du cours dans lequel l’utilisateur navigue. Il vaut le cours « site » par défaut, et c’est require_login($course) qui le met à jour quand une page appartient à un cours. Dans du code moderne, on préfère souvent passer explicitement l’objet cours de fonction en fonction plutôt que de dépendre de ce global — mais vous le croiserez partout dans le code historique :
global $COURSE;
if ($COURSE->id == SITEID) {
// Nous sommes sur la page d'accueil / hors de tout cours.
} else {
echo format_string($COURSE->fullname); // Toujours format_string() pour
// afficher un nom de cours (filtres,
// multilangue, échappement).
}3.8 $SITE — le cours « site »
Particularité historique savoureuse : dans le modèle de données Moodle, la page d’accueil du site est elle-même un cours, de id = 1 (constante SITEID). $SITE contient cet enregistrement ; on l’utilise surtout pour $SITE->fullname et $SITE->shortname dans les en-têtes de pages système. C’est un vestige assumé du Moodle originel où « tout était cours », comme $SITE->id === SITEID le rappelle.
3.9 Pourquoi des globals en 2026 ? Et l’alternative \core\di
Pourquoi diable une base de code active en 2026 repose-t-elle sur global $DB ? Trois raisons honnêtes :
- L’histoire : en 2002, c’était le pattern PHP standard. Le code s’est accumulé par strates (des dizaines de milliers d’usages de
$DBdans le core seul). - La rétrocompatibilité : des milliers de plugins tiers utilisent ces globals. Les supprimer casserait l’écosystème, qui est la principale richesse de Moodle.
- Le pragmatisme : dans un modèle shared-nothing où tout meurt en fin de requête, les dangers classiques des singletons (état partagé entre requêtes, fuites) n’existent pas. Un global PHP par requête est, en pratique, un « scope de requête » — moins toxique qu’il n’y paraît.
Cela dit, Moodle modernise : depuis Moodle 4.4, un container d’injection de dépendances conforme PSR-11 est disponible : \core\di. Il permet d’obtenir (et surtout de remplacer dans les tests) les services sans toucher aux globals :
// Style moderne (Moodle 4.4+) : récupérer un service via le container.
$db = \core\di::get(\moodle_database::class); // La même instance que $DB.
$clock = \core\di::get(\core\clock::class); // Horloge injectable — fini
// les time() intestables !
$now = $clock->time();
// Dans une classe moderne, on préfère l'injection par constructeur :
namespace local_monplugin;
class item_manager {
public function __construct(
/** @var \moodle_database Le service BDD, injecté par le container. */
protected readonly \moodle_database $db,
protected readonly \core\clock $clock,
) {
}
public function touch(int $itemid): void {
$this->db->set_field('local_monplugin_items', 'timemodified',
$this->clock->time(), ['id' => $itemid]);
}
}
// Instanciation : le container résout les dépendances du constructeur.
$manager = \core\di::get(\local_monplugin\item_manager::class);L’immense intérêt apparaît dans les tests unitaires : \core\di::set(\core\clock::class, new \incrementing_clock()) remplace le service pour tout le test. Le core migre progressivement ses APIs vers ce modèle ; pour du code neuf, c’est la voie recommandée. Pour du code de page script ou de fonctions legacy, global $DB; reste parfaitement acceptable et idiomatique.
💡 Pour un dev React :
$DB,$USER,$PAGE… sont l’équivalent moral d’un contexte React global (ou de singletons de module côté Node) : un état ambiant accessible partout sans le passer en props. Le mot-cléglobal $DB;joue le rôle du hookuseContext(DBContext)— une déclaration explicite de consommation. Et\core\diest l’arrivée du pattern « provider » testable : comme quand vous enveloppez vos tests React dans un<Provider value={mock}>,\core\di::set()substitue l’implémentation. La différence culturelle : React a eu les providers dès le départ ; Moodle les greffe sur 20 ans d’existant.
📚 Aller plus loin : moodledev.io/docs/apis/core/di pour le container, et la page « Coding style » de moodledev.io pour les conventions d’usage des globals.
4. Frankenstyle : la convention de nommage universelle
4.1 Le principe
Chaque composant de Moodle — chaque plugin, chaque sous-système du core — possède un nom canonique unique appelé son frankenstyle (le terme est un hommage interne au développeur Petr Škoda, alias « skodak », qui a formalisé la convention). Le format est :
[type-de-plugin]_[nom]Exemples : mod_quiz (l’activité Quiz), block_html (le bloc HTML), theme_boost (le thème Boost), local_mareporting (un plugin « local » maison), auth_oauth2, enrol_manual, qtype_multichoice, tool_dataprivacy (les outils d’administration ont le préfixe tool_), report_log, format_topics, tiny_link (plugin de l’éditeur TinyMCE).
Le core lui-même suit la convention : le noyau s’appelle core, et ses sous-systèmes core_[subsystem] : core_course, core_user, core_files, core_message, core_completion…
4.2 Les types de plugins principaux
Il existe plus de 40 types de plugins ; voici les incontournables avec leur dossier (sous public/ depuis 5.1) :
| Type (préfixe) | Dossier | Rôle | Exemple |
|---|---|---|---|
mod | mod/ | Activités et ressources de cours | mod_quiz, mod_forum, mod_assign |
block | blocks/ | Blocs latéraux/tableau de bord | block_html, block_recentlyaccesseditems |
local | local/ | Fourre-tout : extensions libres sans contrat imposé | local_monoutil |
theme | theme/ | Thèmes graphiques | theme_boost |
auth | auth/ | Méthodes d’authentification | auth_oauth2, auth_ldap |
enrol | enrol/ | Méthodes d’inscription aux cours | enrol_manual, enrol_self |
qtype | question/type/ | Types de questions | qtype_multichoice |
qbank | question/bank/ | Plugins de banque de questions | qbank_managecategories |
tool | admin/tool/ | Outils d’administration | tool_dataprivacy, tool_uploaduser |
report | report/ | Rapports du site | report_log |
coursereport/gradereport | grade/report/ | Rapports de notes | gradereport_grader |
format | course/format/ | Formats de cours | format_topics, format_weeks |
editor / tiny | lib/editor/ / lib/editor/tiny/plugins/ | Éditeurs de texte riches et leurs plugins | editor_tiny, tiny_equation |
filter | filter/ | Filtres de contenu | filter_multilang |
repository | repository/ | Sources de fichiers | repository_upload |
webservice | webservice/ | Protocoles de web services | webservice_rest |
(L’éditeur historique Atto, atto_* dans lib/editor/atto/, a été retiré du core en faveur de TinyMCE — vous croiserez encore son nom dans du code et des docs tierces.)
Notez la subtilité du mapping : le préfixe tool_ correspond au dossier admin/tool/, qtype_ à question/type/, format_ à course/format/. La table de correspondance officielle vit dans lib/components.json (§ 6).
💡 Pour un dev React : le frankenstyle est l’exact équivalent des packages npm scopés :
mod_quiz≈@mod/quiz,core_course≈@core/course. Comme un nom de package npm, le frankenstyle est l’identifiant sous lequel un composant est référencé partout : imports (namespaces), assets (templates, chaînes de langue), registre (le répertoire de plugins moodle.org ≈ npm registry). Et comme npm imposepackage.json, chaque plugin Moodle doit déclarer son identité dansversion.php($plugin->component = 'local_monplugin';).
4.3 Le frankenstyle est PARTOUT
C’est le point clé : cette convention n’est pas décorative, elle structure mécaniquement tout le framework. Le même identifiant mod_quiz gouverne :
// 1. Les NAMESPACES PHP (autoloading, § 5) :
$attempt = new \mod_quiz\quiz_attempt(/* … */);
// 2. Les TABLES de base de données (préfixées par le nom SANS le type
// pour les modules d'activité, AVEC pour les autres) :
// mdl_quiz, mdl_quiz_attempts (mod_quiz — exception historique des mods)
// mdl_local_monplugin_items (local_monplugin)
// 3. Les CHAÎNES DE LANGUE : fichier mod/quiz/lang/en/quiz.php,
// consommées par get_string('identifiant', 'composant') :
echo get_string('attemptquiznow', 'mod_quiz');
// 4. Les CAPABILITIES (permissions), au format type/nom:action :
require_capability('mod/quiz:attempt', $context);
// 5. Les TEMPLATES Mustache : mod/quiz/templates/foo.mustache →
echo $OUTPUT->render_from_template('mod_quiz/foo', $data);
// 6. Les modules JAVASCRIPT AMD/ESM : mod/quiz/amd/src/main.js →
$PAGE->requires->js_call_amd('mod_quiz/main', 'init');
// 7. La CONFIG de plugin :
$grademethod = get_config('mod_quiz', 'grademethod');
// 8. Les ÉVÉNEMENTS, TÂCHES, CACHES, WEB SERVICES… tous namespacés pareil.Une fois ce réflexe acquis, naviguer dans Moodle devient mécanique : vous voyez get_string('pluginname', 'tool_dataprivacy') → vous savez que la chaîne est définie dans admin/tool/dataprivacy/lang/en/tool_dataprivacy.php. Vous voyez le template core_course/activity_info → fichier course/templates/activity_info.mustache.
4.4 core_component : l’annuaire des composants
La classe core_component (chargée avant tout le reste par setup.php) est l’oracle qui traduit les frankenstyles en chemins et inversement :
// Où vit un composant ? (retourne un chemin absolu ou null)
$dir = core_component::get_component_directory('mod_quiz');
// → /srv/moodle/public/mod/quiz
// Normaliser un nom de composant en [type, nom].
// Gère les noms « paresseux » du code historique :
[$type, $plugin] = core_component::normalize_component('quiz');
// → ['mod', 'quiz'] (sans type explicite, « mod » est supposé — héritage !)
[$type, $plugin] = core_component::normalize_component('tool_dataprivacy');
// → ['tool', 'dataprivacy']
[$type, $plugin] = core_component::normalize_component('course');
// → ['core', 'course'] (sous-système du core)
// Lister tous les plugins d'un type :
$mods = core_component::get_plugin_list('mod');
// → ['assign' => '/srv/moodle/public/mod/assign', 'quiz' => …, …]
// La fonction procédurale historique existe encore :
[$type, $plugin] = normalize_component('quiz');⚠️ Piège : l’ambiguïté historique
quizvsmod_quiz. Beaucoup de vieilles APIs acceptaient le nom court (get_string('x', 'quiz')) en supposant le typemod. Écrivez toujours le frankenstyle complet (mod_quiz) dans votre code : c’est la forme canonique moderne, et la seule non ambiguë (il pourrait exister unblock_quiz!). Même vigilance pour les capabilities : c’estmod/quiz:attempt(avec un slash), pasmod_quiz:attempt.
5. Autoloading : le dossier classes/
5.1 La règle de mapping
Moodle n’utilise pas PSR-4 avec Composer pour son propre code (le dossier vendor/ ne concerne que les dépendances tierces embarquées) : il possède son propre autoloader, enregistré par core_component, avec une règle de mapping simple et systématique :
\{frankenstyle}\{sous\namespace}\{classe}
↓
{dossier_du_composant}/classes/{sous/namespace}/{classe}.phpConcrètement :
| Classe | Fichier |
|---|---|
\mod_quiz\access_manager | mod/quiz/classes/access_manager.php |
\mod_quiz\output\renderer | mod/quiz/classes/output/renderer.php |
\mod_quiz\form\preflight_check_form | mod/quiz/classes/form/preflight_check_form.php |
\core\di | lib/classes/di.php (le core mappe sur lib/classes/) |
\core\context\module | lib/classes/context/module.php |
\core_course\management\helper | course/classes/management/helper.php (sous-système) |
\local_monplugin\item_manager | local/monplugin/classes/item_manager.php |
Deux cas particuliers à mémoriser : le composant core mappe sur lib/classes/, et chaque sous-système core_xxx mappe sur le dossier classes/ de son répertoire attitré (core_course → course/classes/, core_user → user/classes/…). Signalons au passage un exemple de modernisation par déplacement : depuis Moodle 4.2, les classes de contexte ont déménagé de lib/accesslib.php vers le namespace \core\context (context_module::instance($cmid) est devenu \core\context\module::instance($cmid) — les anciens noms restent des alias fonctionnels, mais le code neuf doit utiliser les nouveaux).
💡 Pour un dev React : c’est exactement la résolution de modules de Node/TypeScript, avec le frankenstyle comme alias de chemin.
use \mod_quiz\output\renderer;≈import { Renderer } from '@mod/quiz/output/renderer'. Le dossierclasses/d’un plugin ≈ son dossiersrc/: tout ce qui y est se charge à la demande, sansrequiremanuel, et le nom de fichier doit correspondre au nom exporté. La grosse différence : PHP charge paresseusement à la première utilisation de la classe pendant la requête, il n’y a pas d’étape de bundling.
5.2 Créer une classe autoloadée : exemple complet
Créons une classe utilitaire pour un plugin local fictif local_greeting. Fichier public/local/greeting/classes/greeter.php :
<?php
// (En-tête GPL omis — obligatoire en vrai.)
namespace local_greeting; // ← DOIT être le frankenstyle.
/**
* Fabrique des messages d'accueil personnalisés.
*
* @package local_greeting
* @copyright 2026 Alex Lemia
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class greeter {
/**
* Construit le message d'accueil pour un utilisateur.
*
* @param \stdClass $user Enregistrement utilisateur (table user).
* @return string Message prêt à afficher (déjà échappé).
*/
public static function greet(\stdClass $user): string {
// get_string() avec paramètre : la chaîne contient {$a}.
$message = get_string('welcomeuser', 'local_greeting', fullname($user));
// s() échappe pour la sortie HTML (équivalent de l'échappement JSX).
return \html_writer::div(s($message), 'local-greeting-message');
}
}Utilisation — aucune inclusion manuelle nécessaire, l’autoloader fait tout :
// N'importe où dans Moodle, après le bootstrap :
echo \local_greeting\greeter::greet($USER);Trois règles de style Moodle visibles ici : une classe par fichier, nom de fichier = nom de classe en minuscules, le namespace racine est exactement le frankenstyle (jamais \LocalGreeting\ ni \local\greeting\). Le bloc PHPDoc avec @package est exigé par les vérificateurs de code officiels.
5.3 Le legacy : lib.php, locallib.php et les grandes libs
Tout le code n’est pas autoloadé, loin de là. Trois catégories de fichiers « legacy » coexistent avec classes/ :
lib.php(dans chaque plugin) : le fichier des fonctions de callback historiques que le core appelle par convention de nommage :quiz_add_instance(),quiz_supports(),local_monplugin_extend_navigation()… Le core l’inclut lui-même quand il en a besoin. C’est l’ancêtre du système de hooks (l’API moderne des hooks, introduite en Moodle 4.4, remplace progressivement ces callbacks par des classes d’écouteurs autoloadées — chapitre dédié plus loin).locallib.php(optionnel) : les fonctions internes du plugin, à inclure explicitement (require_once($CFG->dirroot . '/mod/quiz/locallib.php');). Convention pour ne pas alourdirlib.php, que le core charge souvent.- Les grandes libs du core, chargées d’office par
setup.php:lib/moodlelib.php(~9000 lignes de fonctions générales),lib/weblib.php(sortie web),lib/datalib.php,lib/accesslib.php,lib/filelib.php… C’est pour cela queget_string(),required_param()ourequire_login()sont disponibles sans aucun import : ce sont des fonctions globales déjà chargées.
La direction du projet est claire : tout code nouveau va dans classes/ ; lib.php ne doit plus contenir que les callbacks que le core exige encore sous forme de fonctions.
⚠️ Piège : après avoir créé ou renommé un fichier dans
classes/, si Moodle ne trouve pas votre classe, purgez les caches (php admin/cli/purge_caches.phpou Administration → Développement → Purger les caches).core_componentmet en cache la carte des composants et des classes ; en développement, activez$CFG->debug = E_ALL;et surtout le mode développeur des caches pour limiter ces surprises. Le symptôme classique : « Class “\local_monplugin\truc” not found » alors que le fichier existe — 9 fois sur 10, c’est le cache de composants.
6. « Tout est plugin »
Voici l’idée architecturale la plus élégante de Moodle : le core lui-même est construit comme une collection de plugins. Une installation standard de Moodle 5.2 embarque environ 400 plugins « standard » : le forum est un plugin (mod_forum), le quiz est un plugin (mod_quiz), l’authentification par email est un plugin (auth_email), le thème par défaut est un plugin (theme_boost), la sauvegarde des notes en rapport est un plugin (gradereport_grader)…
La différence entre « code du core » et « plugin tiers » n’est pas une différence de nature ni de qualité, mais de contrat :
- Un plugin (standard ou tiers) implémente les contrats d’API de son type : un
mod_doit fournirview.php,lib.phpavecxxx_add_instance(),version.php,db/access.php… Il consomme les APIs du core mais le core ne dépend jamais de lui nommément. - Le core (
lib/, et les sous-systèmes) fournit les APIs transverses et le cadre d’exécution. Il connaît les types de plugins, jamais les plugins individuels.
Cette symétrie a une conséquence pratique énorme pour vous : pour apprendre à écrire un plugin, lisez les plugins standard. mod_folder est un module d’activité minimal parfait comme modèle ; mod_assign montre toutes les APIs avancées. Votre plugin tiers utilisera exactement les mêmes mécanismes que le code « officiel » — aucune API secrète réservée au core.
Distinguez enfin plugin types et subsystems :
- Un plugin type (
mod,block,auth…) est un emplacement extensible : on peut y ajouter des plugins. - Un subsystem (
core_course,core_files,core_message…) est une zone fonctionnelle du core, avec son dossier, ses classes, ses chaînes de langue — mais on ne peut pas y « installer » quoi que ce soit.
La liste officielle et exhaustive des deux vit dans un fichier JSON que vous devriez ouvrir dès maintenant : lib/components.json (sous public/lib/ en 5.1+). Extrait :
{
"plugintypes": {
"mod": "mod",
"auth": "auth",
"block": "blocks",
"qtype": "question/type",
"tool": "admin/tool",
"local": "local"
},
"subsystems": {
"course": "course",
"files": "files",
"message": "message"
}
}C’est ce fichier (plus les db/subplugins.json de certains plugins, car un plugin peut définir ses propres sous-types de plugins — ex. : quizaccess_ pour les règles d’accès au quiz) que core_component lit pour construire sa carte du monde.
💡 Pour un dev React : imaginez que React livre
react-dom,react-routeret 400 composants officiels sous forme de packages npm ordinaires, publiés sur le même registre que les vôtres, respectant les mêmespeerDependencies— et que « faire partie du core » signifie seulement « maintenu par la core team et préinstallé ». C’est le monorepo ultime : le dépôt Moodle est un monorepo de ~400 packages dont les contrats mutuels sont les APIs documentées sur moodledev.io.
📚 Aller plus loin : la liste des types de plugins avec leurs contrats respectifs : moodledev.io/docs/apis/plugintypes ; l’anatomie générale d’un plugin : moodledev.io/docs/apis/commonfiles .
7. Structure d’une page type
7.1 Un view.php complet, ligne par ligne
Assemblons tout ce qui précède. Voici la page principale d’un plugin local local_greeting — le squelette que vous réécrirez cent fois. Fichier public/local/greeting/index.php :
<?php
// (En-tête GPL v3 omis pour la lisibilité — obligatoire en réalité.)
/**
* Page principale du plugin local_greeting.
*
* @package local_greeting
* @copyright 2026 Alex Lemia
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
// ── 1. BOOTSTRAP ─────────────────────────────────────────────────────────
// Toujours la PREMIÈRE ligne. Le chemin relatif remonte jusqu'à config.php.
// Depuis local/greeting/, config.php est deux niveaux plus haut… puis Moodle
// résout lui-même son emplacement réel (racine de l'installation en 5.1+).
require(__DIR__ . '/../../config.php');
// ── 2. PARAMÈTRES ────────────────────────────────────────────────────────
// JAMAIS $_GET/$_POST directement : required_param()/optional_param()
// valident ET nettoient selon un type déclaré (PARAM_INT, PARAM_ALPHA,
// PARAM_TEXT…). required_param() stoppe la page si le paramètre manque.
$courseid = required_param('courseid', PARAM_INT);
$filter = optional_param('filter', 'all', PARAM_ALPHA);
// ── 3. CHARGEMENT DES OBJETS MÉTIER ─────────────────────────────────────
// MUST_EXIST → moodle_exception (page d'erreur propre) si le cours n'existe pas.
$course = $DB->get_record('course', ['id' => $courseid], '*', MUST_EXIST);
// ── 4. AUTHENTIFICATION ──────────────────────────────────────────────────
// require_login() : vérifie la session, force la connexion si nécessaire,
// vérifie l'inscription au cours passé en argument, met à jour $COURSE,
// déclenche les événements de log. OBLIGATOIRE sur toute page (sauf les
// rarissimes pages volontairement publiques, qui l'indiquent explicitement).
require_login($course);
// ── 5. CONTEXTE ET PERMISSIONS ──────────────────────────────────────────
// Le « contexte » situe la page dans la hiérarchie de sécurité de Moodle
// (système > catégorie > cours > module > utilisateur/bloc). Chapitre dédié.
// Rappel : classes déplacées dans \core\context depuis Moodle 4.2.
$context = \core\context\course::instance($course->id);
// Vérification de permission : stoppe avec « Accès refusé » si l'utilisateur
// n'a pas la capability (déclarée dans db/access.php du plugin).
require_capability('local/greeting:view', $context);
// ── 6. CONFIGURATION DE $PAGE ────────────────────────────────────────────
// À faire AVANT toute sortie HTML.
$PAGE->set_url(new moodle_url('/local/greeting/index.php', [
'courseid' => $courseid, // L'URL CANONIQUE de cette page, avec
'filter' => $filter, // ses paramètres : sert aux logs, au
])); // bouton retour, au fil d'Ariane…
$PAGE->set_context($context);
$PAGE->set_title(get_string('pluginname', 'local_greeting'));
$PAGE->set_heading(format_string($course->fullname));
$PAGE->set_pagelayout('incourse'); // Layout « dans un cours ».
// ── 7. LOGIQUE MÉTIER (avant toute sortie !) ─────────────────────────────
// Traiter les actions, préparer les données. Si un redirect() doit avoir
// lieu (après un POST par exemple), c'est ICI, avant le header.
$greeting = \local_greeting\greeter::greet($USER);
// ── 8. SORTIE ────────────────────────────────────────────────────────────
echo $OUTPUT->header(); // Tout le haut de page du thème.
echo $OUTPUT->heading(get_string('welcomeheading', 'local_greeting'));
echo $greeting;
if ($filter === 'all') {
echo $OUTPUT->notification(
get_string('showingall', 'local_greeting'),
\core\output\notification::NOTIFY_INFO
);
}
echo $OUTPUT->footer(); // Pied de page + JS différé. FIN.Mémorisez cette liturgie en huit temps — config.php → params → objets → require_login → contexte/capabilities → $PAGE → logique → $OUTPUT — car toutes les pages Moodle la suivent, du core le plus ancien au plugin le plus récent.
⚠️ Piège : tout
redirect()doit intervenir avantecho $OUTPUT->header(). Une fois le header émis, les en-têtes HTTP sont partis, et Moodle devra dégrader la redirection en page intermédiaire (voire échouer en mode debug avec « unsupported redirect detected »). Structurez donc toujours vos pages en « traiter d’abord, afficher ensuite » — le pattern POST-redirect-GET est la norme pour tout formulaire.
⚠️ Piège : ne construisez jamais vos URLs en concaténant des chaînes (
$CFG->wwwroot . '/local/greeting/index.php?courseid=' . $id). Utilisez toujoursnew moodle_url('/local/greeting/index.php', ['courseid' => $id]): échappement correct des paramètres, compatibilité avec$PAGE->set_url(), et vos URLs survivront aux évolutions (routeur, déplacements).
7.2 Variante moderne : sortir le HTML de la page
Le echo de HTML dans le page script est le style historique. Le style moderne sépare la préparation des données (une classe templatable/renderable ou un renderer) du gabarit (template Mustache). Aperçu — le chapitre 04 y consacrera des dizaines de pages :
// … mêmes étapes 1 à 7, puis :
echo $OUTPUT->header();
// Le page script ne fabrique plus de HTML : il rend un template avec un
// contexte de données préparé. templates/main.mustache dans le plugin.
echo $OUTPUT->render_from_template('local_greeting/main', [
'greeting' => \local_greeting\greeter::greet($USER),
'coursename' => format_string($course->fullname),
'showfilterinfo' => ($filter === 'all'),
]);
echo $OUTPUT->footer();Le page script devient alors un pur « contrôleur » de vingt lignes ; le HTML vit dans templates/main.mustache, surchargeable par les thèmes et réutilisable côté JavaScript. C’est la cible stylistique de tout code neuf.
7.3 Les points d’entrée qui ne sont pas des pages
Toute exécution de Moodle ne passe pas par un page script. Trois autres familles de points d’entrée, chacune avec son bootstrap adapté :
- Les scripts CLI (
admin/cli/*.php, etcli/dans les plugins) : cron, purge de caches, upgrade, installation… Ils définissentdefine('CLI_SCRIPT', true);avant lerequire(config.php), ce qui indique àsetup.phpde ne pas démarrer de session web ni de vérifier d’utilisateur.
<?php
define('CLI_SCRIPT', true);
require(__DIR__ . '/../../config.php');
require_once($CFG->libdir . '/clilib.php');
[$options, $unrecognised] = cli_get_params(['help' => false], ['h' => 'help']);
cli_writeln('Traitement en cours…');- Les web services (
webservice/rest/server.php, et l’API Ajax internelib/ajax/service.php) : les appels AJAX du frontend Moodle et les apps mobiles passent par des fonctions externes déclarées, pas par des page scripts.define('AJAX_SCRIPT', true);adapte le bootstrap (réponses JSON, gestion d’erreurs sérialisée). Chapitre dédié aux web services plus loin dans le cours. pluginfile.php: toutes les URLs de fichiers utilisateur (/pluginfile.php/…) passent par ce script unique, qui vérifie les permissions puis délègue au plugin propriétaire du fichier via un callback. Jamais de lien direct versdataroot— c’est le contrôle d’accès aux fichiers de Moodle.
💡 Pour un dev React : cette trilogie vous est familière : page scripts ≈ pages SSR, web services/Ajax ≈ vos API routes (
app/api/…/route.ts), scripts CLI ≈ vos scriptsnpm run …exécutés hors serveur.pluginfile.php, lui, ressemble à une API route de fichiers avec contrôle d’accès, comme un handler Next.js qui streame un fichier S3 après vérification de session.
8. Component library & outils du développeur
Pour clore ce tour d’horizon, l’outillage qui rendra les chapitres suivants concrets :
La Component Library : Moodle embarque son « Storybook » — un catalogue interactif de tous les composants d’interface (boutons, badges, notifications, templates core, avec leurs variantes Bootstrap adaptées à Moodle). Deux accès : la version en ligne liée à moodledev.io, ou la version locale de votre installation (elle documente alors vos composants avec votre thème) : installez les dépendances Node du dépôt puis lancez npx grunt componentlibrary, et ouvrez /admin/tool/componentlibrary/. Réflexe à prendre : avant de coder un élément d’UI, vérifiez qu’il n’existe pas déjà dans la component library.
Le Moodle Plugin Skeleton Generator (tool_pluginskel) : un plugin d’administration qui génère l’arborescence complète et conforme d’un nouveau plugin (fichiers obligatoires, version.php, chaînes de langue, db/access.php…) depuis un formulaire web ou la CLI. Le meilleur moyen de démarrer sans oublier un fichier rituel. Vous le trouverez dans le répertoire officiel des plugins (moodle.org/plugins).
XDebug : avec l’architecture page scripts, le débogueur pas-à-pas est roi. Configurez XDebug + votre IDE (PhpStorm ou VS Code), posez un point d’arrêt sur la première ligne après require(config.php) d’une page, et regardez $CFG, $USER, $PAGE se remplir : une session de debug sur course/view.php vous apprendra plus que dix articles. Complétez avec $CFG->debug = (E_ALL); et $CFG->debugdisplay = 1; en dev — Moodle devient alors très bavard sur vos erreurs d’API (paramètres non nettoyés, chaînes manquantes, requêtes par boucle…).
Lire le code efficacement : le dépôt fait des dizaines de milliers de fichiers ; on ne le lit pas, on le requête. Vos meilleurs points d’entrée : (1) le plugin standard le plus proche de ce que vous voulez faire, (2) la recherche plein-texte sur un nom de fonction ou de capability, (3) la référence PHPDoc générée sur phpdoc.moodledev.io , (4) le code des tests (tests/ dans chaque composant) qui montre l’usage canonique de chaque API.
📚 Aller plus loin : le portail développeur moodledev.io est votre documentation de référence permanente — en particulier « Getting started », le « Guide to Moodle coding style », les release notes développeur de chaque version (moodledev.io/docs/5.2/devupdate ) et la page Component Library. Gardez aussi sous le coude le forum « General developer forum » de moodle.org, où la core team répond réellement.
9. Résumé — points clés
- Moodle n’est pas un MVC : c’est une architecture « page scripts + APIs transverses » née en 2002. Une URL = un fichier PHP physique (
mod/quiz/view.php), qui orchestre des APIs de framework très riches. Un routeur moderne (\core\router, Slim/PSR, depuis 4.5) migre progressivement certaines pages, mais le page script reste le modèle dominant en 5.2. - Toute page commence par
require(__DIR__ . '/../config.php'):config.phpdéfinit$CFG(BDD,wwwroot,dataroot, préfixemdl_, forced settings) puis inclutlib/setup.php, qui charge les libs du core, enregistre l’autoloader, connecte$DB, démarre la session et peuple$USER,$PAGE,$OUTPUT. - Trois racines à ne pas confondre :
wwwroot(URL publique),dirroot(code servi),dataroot(données, hors web, en écriture). Depuis Moodle 5.1, le code servi vit danspublic/(DocumentRoot =public/),$CFG->rootpointe la racine de l’installation ;wwwroot/dirrootgardent leur sémantique. - Huit globals sacrés :
$CFG,$DB,$USER,$SESSION,$PAGE,$OUTPUT,$COURSE,$SITE. Dans une fonction :global $DB;obligatoire. L’alternative moderne et testable est le container PSR-11\core\di(depuis 4.4) :\core\di::get(\moodle_database::class). - Frankenstyle = identifiant universel
[type]_[nom](mod_quiz,tool_dataprivacy,core_course). Il gouverne namespaces, tables,get_string(), capabilities (mod/quiz:attempt), templates, config, JS.core_componenttraduit frankenstyle ↔ chemins. - Autoloading :
\mod_quiz\foo\bar→mod/quiz/classes/foo/bar.php; core →lib/classes/; sous-systèmes →<dossier>/classes/. Le legacy (lib.phppour les callbacks,locallib.php, grandes libs procédurales) coexiste ; tout code neuf va dansclasses/. Pensez à purger les caches après création de classes. Classes de contexte dans\core\contextdepuis 4.2 ; hooks modernes depuis 4.4. - Tout est plugin : ~400 plugins standard composent le core ; la différence core/tiers est contractuelle, pas qualitative. Types de plugins (extensibles) vs subsystems (zones du core) : liste dans
lib/components.json. Lisez les plugins standard comme modèles. - Liturgie d’une page : config →
required_param→ objets métier →require_login→ contexte +require_capability→$PAGE->set_*→ logique (et redirections) →$OUTPUT->header()/ contenu (idéalement via template Mustache) /$OUTPUT->footer(). Autres points d’entrée : CLI (CLI_SCRIPT), web services/Ajax (AJAX_SCRIPT),pluginfile.php. - Outillage : Component Library (le « Storybook » de Moodle),
tool_pluginskelpour générer des squelettes, XDebug pour suivre le cycle de vie, moodledev.io et phpdoc.moodledev.io comme références permanentes.
Chapitre suivant : 02 — La base de données