Skip to Content

Terrain 13 — Premier plugin (local)

⏱️ TL;DR — 30 mini-jeux ⭐⭐⭐ pour construire local_todolist (une todo-list par utilisateur) brique par brique : version.php, langue, table XMLDB, capabilities, page sécurisée, Form API, DML (CRUD), Output API + Mustache, navigation, réglages, chaîne d’upgrade. C’est le projet Niveau 2 en version mini-jeux, et le socle de tout dev Moodle.

⚠️ Travaille sur une instance de dev (terrain 12), mode DEVELOPER actif, et purge les caches après chaque changement structurel.


🕹️ Mini-jeu 1 — Choisir le type de plugin ⭐⭐⭐ 💻

But : local vs autre. Mission : justifie pourquoi une todo-list transverse est un plugin local.

✅ Solution

Une fonctionnalité transverse (pas une activité de cours, pas un bloc) → type local. Frankenstyle : local_todolist, dossier local/todolist/. Le bon choix de type évite de tout refaire (terrain 06 du cours).


🕹️ Mini-jeu 2 — version.php ⭐⭐⭐ 💻

But : la carte d’identité. Mission : crée local/todolist/version.php. Étapes :

<?php defined('MOODLE_INTERNAL') || die(); $plugin->component = 'local_todolist'; $plugin->version = 2026070900; $plugin->requires = 2025100700; $plugin->maturity = MATURITY_ALPHA;

✅ Solution

version.php = le package.json de Moodle : component (frankenstyle), version (YYYYMMDDXX, à bumper à chaque changement de schéma), requires (version min de Moodle), maturity. defined('MOODLE_INTERNAL') || die(); protège l’accès direct.


🕹️ Mini-jeu 3 — Le fichier de langue ⭐⭐⭐ 💻

But : nommer le plugin. Mission : crée local/todolist/lang/en/local_todolist.php. Étapes :

<?php $string['pluginname'] = 'Todo list'; $string['mytasks'] = 'My tasks'; $string['addtask'] = 'Add a task';

✅ Solution

Toute chaîne visible vit dans lang/en/ (jamais en dur). pluginname est obligatoire. On les appelle avec get_string('addtask', 'local_todolist'). Base de l’i18n (terrain 16). D’autres langues = d’autres dossiers.


🕹️ Mini-jeu 4 — Installer le plugin vide ⭐⭐ 💻

But : le voir apparaître. Mission : installe local_todolist via Notifications.

✅ Solution

Avec version.php + lang/, Moodle détecte le plugin (page Notifications) et l’installe. Il apparaît dans la vue d’ensemble des plugins. Rien ne s’affiche encore (pas de page) — c’est normal.


🕹️ Mini-jeu 5 — La table XMLDB ⭐⭐⭐⭐ 💻

But : persister les tâches. Mission : crée db/install.xml avec une table local_todolist (id, userid, task, done, timecreated) via l’éditeur XMLDB.

✅ Solution

Via l’éditeur XMLDB (terrain 12), définis les champs et les index (dont userid en clé étrangère/index). Il génère db/install.xml portable (MySQL/PostgreSQL). Bumpe la version puis installe → la table est créée. Jamais de CREATE TABLE à la main.


🕹️ Mini-jeu 6 — La capability ⭐⭐⭐⭐ 💻

But : un droit dédié. Mission : crée db/access.php avec local/todolist:manage. Étapes :

<?php $capabilities = [ 'local/todolist:manage' => [ 'captype' => 'write', 'contextlevel' => CONTEXT_USER, 'archetypes' => ['user' => CAP_ALLOW], ], ];

✅ Solution

db/access.php déclare les capabilities du plugin (terrain 11). contextlevel + archetypes définissent où et à qui elle s’applique par défaut. Bumpe la version + purge pour l’enregistrer. Tu la vérifieras dans le code (jeu 8).


🕹️ Mini-jeu 7 — La page index.php sécurisée ⭐⭐⭐⭐ 💻

But : le pattern d’entrée. Mission : crée index.php avec le pattern de sécurité complet. Étapes :

<?php require('../../config.php'); require_login(); $context = context_user::instance($USER->id); $PAGE->set_context($context); $PAGE->set_url(new moodle_url('/local/todolist/index.php')); $PAGE->set_title(get_string('pluginname', 'local_todolist')); echo $OUTPUT->header(); echo $OUTPUT->footer();

✅ Solution

Le pattern sacré : config.phprequire_login()contexte$PAGE->set_*$OUTPUT->header()/footer(). Sans lui, pas de sécurité ni de rendu correct. Toute page de plugin commence ainsi. $USER, $PAGE, $OUTPUT, $DB sont des globals Moodle.


🕹️ Mini-jeu 8 — Vérifier la capability ⭐⭐⭐⭐ 💻

But : protéger l’accès. Mission : ajoute require_capability à index.php. Étapes :

require_capability('local/todolist:manage', $context);

✅ Solution

require_capability($cap, $context) bloque l’accès si l’utilisateur n’a pas le droit dans ce contexte. C’est la garde serveur (terrain 11, jeu 18). Cacher un lien ne suffit jamais — cette ligne, si.


🕹️ Mini-jeu 9 — La Form API ⭐⭐⭐⭐ 💻

But : un formulaire propre. Mission : crée classes/form/task_form.php (moodleform). Étapes :

<?php namespace local_todolist\form; class task_form extends \moodleform { public function definition() { $mform = $this->_form; $mform->addElement('text', 'task', get_string('addtask', 'local_todolist')); $mform->setType('task', PARAM_TEXT); $this->add_action_buttons(); } }

✅ Solution

La Form API (moodleform) génère des formulaires sécurisés (sesskey/CSRF automatique, validation, types PARAM_*). On ne bricole jamais <form> à la main. setType assainit chaque champ. Base de toute saisie.


🕹️ Mini-jeu 10 — Afficher le formulaire ⭐⭐⭐ 💻

But : le brancher à la page. Mission : instancie et affiche le formulaire dans index.php. Étapes :

$mform = new \local_todolist\form\task_form(); echo $OUTPUT->header(); $mform->display(); echo $OUTPUT->footer();

✅ Solution

On instancie la classe (autoload via le namespace local_todolist\form) et display(). Le formulaire s’affiche avec son bouton et son sesskey intégré. L’autoloading Moodle charge la classe depuis classes/ sans require.


🕹️ Mini-jeu 11 — Traiter la soumission ⭐⭐⭐⭐ 💻

But : récupérer les données. Mission : traite get_data() du formulaire. Étapes :

if ($data = $mform->get_data()) { // insérer (jeu 12) redirect($PAGE->url); }

✅ Solution

get_data() renvoie les données validées et assainies (ou null si non soumis/invalide). Après traitement, on redirige (pattern POST-redirect-GET) pour éviter la re-soumission. La Form API a déjà vérifié le sesskey.


🕹️ Mini-jeu 12 — Insérer en base (DML) ⭐⭐⭐⭐ 💻

But : créer une tâche. Mission : insère la tâche via $DB. Étapes :

global $DB, $USER; $record = (object)[ 'userid' => $USER->id, 'task' => $data->task, 'done' => 0, 'timecreated' => time(), ]; $DB->insert_record('local_todolist', $record);

✅ Solution

Le DML ($DB->insert_record) écrit sans SQL brut, en gérant l’échappement et la portabilité. On passe le nom court de table (local_todolist, sans préfixe). Jamais de concaténation SQL. API de base de données de Moodle (Partie 5).


🕹️ Mini-jeu 13 — Lister par utilisateur ⭐⭐⭐⭐ 💻

But : afficher ses tâches. Mission : récupère les tâches de l’utilisateur courant. Étapes :

$tasks = $DB->get_records('local_todolist', ['userid' => $USER->id], 'timecreated DESC');

✅ Solution

get_records($table, $conditions, $sort) renvoie un tableau d’objets filtré par userid → chacun ne voit que ses tâches (isolation par utilisateur). Les conditions sont paramétrées (sûres). Jamais WHERE userid = $USER->id concaténé.


🕹️ Mini-jeu 14 — Mettre à jour ⭐⭐⭐⭐ 💻

But : cocher une tâche. Mission : passe done à 1 pour une tâche. Étapes :

$task = $DB->get_record('local_todolist', ['id' => $id, 'userid' => $USER->id], '*', MUST_EXIST); $task->done = 1; $DB->update_record('local_todolist', $task);

✅ Solution

On re-vérifie userid dans la condition (un utilisateur ne modifie que ses tâches — sécurité par la donnée). MUST_EXIST lève une exception si absent. update_record attend l’objet complet avec son id.


🕹️ Mini-jeu 15 — Supprimer avec sesskey ⭐⭐⭐⭐ 💻

But : action destructive protégée. Mission : supprime une tâche, protégée par sesskey. Étapes :

require_sesskey(); $DB->delete_records('local_todolist', ['id' => $id, 'userid' => $USER->id]);

✅ Solution

Toute action destructive via un lien (GET) exige require_sesskey() (anti-CSRF). Le lien doit inclure sesskey() : .../index.php?del=5&sesskey=<?= sesskey() ?>. On filtre encore par userid. Sécurité en profondeur (terrain 16).


🕹️ Mini-jeu 16 — Récupérer les paramètres ⭐⭐⭐⭐ 💻

But : lire l’URL en sécurité. Mission : lis un id et une action depuis l’URL. Étapes :

$id = optional_param('del', 0, PARAM_INT); $act = optional_param('action', '', PARAM_ALPHA);

✅ Solution

required_param/optional_param + un type PARAM_* (INT, ALPHA, TEXT, RAW…) lisent et assainissent les entrées. On ne touche jamais $_GET/$_POST directement. Le type est la première ligne de défense (terrain 16).


🕹️ Mini-jeu 17 — Le renderer ⭐⭐⭐⭐ 💻

But : séparer logique et affichage. Mission : crée classes/output/renderer.php. Étapes :

<?php namespace local_todolist\output; class renderer extends \plugin_renderer_base { public function render_tasklist(tasklist $list) { return $this->render_from_template('local_todolist/tasklist', $list->export_for_template($this)); } }

✅ Solution

L’Output API sépare la logique (renderable) du HTML (template Mustache). Le renderer appelle render_from_template. Approche testable et thémable — bien plus propre que du HTML dans le contrôleur. (Partie 5/8 du cours.)


🕹️ Mini-jeu 18 — Le template Mustache ⭐⭐⭐ 💻

But : le HTML. Mission : crée templates/tasklist.mustache. Étapes :

<ul class="local-todolist"> {{#tasks}} <li class="{{#done}}done{{/done}}">{{task}}</li> {{/tasks}} </ul>

✅ Solution

Les templates Mustache (logique-less) rendent le HTML depuis des données. {{#tasks}}...{{/tasks}} boucle ; {{task}} est auto-échappé (sécurité XSS gratuite). Mêmes templates côté PHP et JS (terrain 14). Standard d’affichage moderne de Moodle.


🕹️ Mini-jeu 19 — export_for_template ⭐⭐⭐⭐ 💻

But : préparer les données du template. Mission : implémente export_for_template() sur un renderable tasklist. Étapes :

public function export_for_template(\renderer_base $output) { return ['tasks' => array_values($this->tasks)]; }

✅ Solution

export_for_template() transforme tes objets en un tableau/stdClass simple que Mustache consomme. array_values réindexe (Mustache attend une liste, pas un map). Contrat clair entre données et vue.


🕹️ Mini-jeu 20 — Obtenir le renderer ⭐⭐⭐ 💻

But : rendre dans la page. Mission : appelle le renderer depuis index.php. Étapes :

$output = $PAGE->get_renderer('local_todolist'); echo $output->render_tasklist(new \local_todolist\output\tasklist($tasks));

✅ Solution

$PAGE->get_renderer('local_todolist') récupère ton renderer (surchargable par un thème !). On lui passe le renderable → HTML. Cette indirection rend l’affichage thémable et testable. Cœur de l’Output API.


🕹️ Mini-jeu 21 — Ajouter à la navigation ⭐⭐⭐⭐ 💻

But : un point d’entrée. Mission : ajoute un lien « Todo list » via un callback de navigation.

✅ Solution

Un callback (local_todolist_extend_navigation dans lib.php, ou un hook moderne) ajoute une entrée au menu. Sinon, le plugin est « invisible ». Les hooks remplacent progressivement les anciens callbacks nommés (Partie 5).


🕹️ Mini-jeu 22 — Une page de réglages ⭐⭐⭐⭐ 💻

But : config admin. Mission : crée settings.php avec un réglage (ex. nb max de tâches). Étapes :

<?php defined('MOODLE_INTERNAL') || die(); if ($hassiteconfig) { $settings = new admin_settingpage('local_todolist', get_string('pluginname', 'local_todolist')); $settings->add(new admin_setting_configtext('local_todolist/maxtasks', 'Max tasks', '', 50, PARAM_INT)); $ADMIN->add('localplugins', $settings); }

✅ Solution

settings.php ajoute une page dans Administration → Plugins. admin_setting_configtext stocke une config lue avec get_config('local_todolist', 'maxtasks'). Framework de réglages standard (pas de table à gérer).


🕹️ Mini-jeu 23 — Lire un réglage ⭐⭐⭐ 💻

But : utiliser la config. Mission : lis maxtasks dans le code. Étapes :

$max = get_config('local_todolist', 'maxtasks');

✅ Solution

get_config($component, $name) lit un réglage plugin ; set_config l’écrit. On limite alors l’ajout de tâches au max configuré. La config vit dans mdl_config_plugins. Paramètre le comportement du plugin.


🕹️ Mini-jeu 24 — La chaîne d’upgrade ⭐⭐⭐⭐⭐ 💻

But : faire évoluer le schéma. Mission : ajoute une colonne priority en version 2 via db/upgrade.php. Étapes :

function xmldb_local_todolist_upgrade($oldversion) { global $DB; $dbman = $DB->get_manager(); if ($oldversion < 2026071000) { $table = new xmldb_table('local_todolist'); $field = new xmldb_field('priority', XMLDB_TYPE_INTEGER, '2', null, null, null, '0', 'done'); if (!$dbman->field_exists($table, $field)) { $dbman->add_field($table, $field); } upgrade_plugin_savepoint(true, 2026071000, 'local', 'todolist'); } return true; }

✅ Solution

db/upgrade.php migre la base entre versions sans perte de données (le if ($oldversion < ...) garde l’idempotence). On bumpe version.php à 2026071000, on lance l’upgrade → la colonne s’ajoute. upgrade_plugin_savepoint marque l’étape. C’est la migration Prisma de Moodle.


🕹️ Mini-jeu 25 — Appliquer l’upgrade ⭐⭐⭐ 💻

But : exécuter la migration. Mission : lance l’upgrade et vérifie la nouvelle colonne.

✅ Solution

Après avoir bumpé la version : page Notifications (ou php admin/cli/upgrade.php) applique xmldb_..._upgrade. Vérifie la colonne dans Adminer. Purge les caches ensuite. Les données existantes sont conservées.


🕹️ Mini-jeu 26 — Un privacy provider ⭐⭐⭐⭐⭐ 💻

But : conformité RGPD. Mission : crée classes/privacy/provider.php déclarant la table.

✅ Solution

Le Privacy API exige un provider qui déclare les données perso stockées (metadata), les exporte et les supprime sur demande (terrain 10, jeu 12). Sans lui, l’installation d’un plugin est signalée non conforme. Obligatoire pour publier (Partie 11).


🕹️ Mini-jeu 27 — Purger et déboguer ⭐⭐⭐ 💻

But : le cycle de dev. Mission : après un changement, purge les caches et utilise debugging().

✅ Solution

Le cycle : code → bumpe version (si db//version.php) → upgrade → purge caches → teste avec debugging() (terrain 12). 90 % des « ça ne marche pas » viennent d’un cache non purgé ou d’une version non bumpée.


🕹️ Mini-jeu 28 — Désinstaller proprement ⭐⭐⭐ 💻

But : un plugin qui sait partir. Mission : désinstalle local_todolist et vérifie que la table part.

✅ Solution

À la désinstallation (vue d’ensemble des plugins), Moodle supprime automatiquement les tables déclarées dans install.xml et les capabilities. Pour du nettoyage custom (config externe), on ajoute db/uninstall.php. Plugin propre = plugin réversible.


🕹️ Mini-jeu 29 — Passer le coding style ⭐⭐⭐⭐ 💻

But : conformité. Mission : lance moodle-cs sur local/todolist.

✅ Solution

Le linter moodle-cs (terrain 12) vérifie indentation, PHPDoc, en-tête GPL, sécurité. Vise zéro erreur : c’est ce que la review moodle.org exige (Partie 11). L’exécuter tôt évite une grosse dette à la fin.


🕹️ Mini-jeu 30 — Récap local_todolist ⭐⭐⭐ 💻

But : capstone du terrain. Mission : assemble tout : version + langue + XMLDB + capability + page sécurisée + Form API + CRUD DML (isolé par user) + Output/Mustache + navigation + réglages + upgrade v2 + privacy — coding style vert.

✅ Solution

Tu as construit un vrai plugin Moodle de bout en bout — c’est local_todolist du cours (Partie 6.3) et du projet Niveau 2. Tu tiens la mécanique commune à tout dev Moodle. Prochaine brique : les blocs et le frontend (terrain 14).


Terrain suivant : 14 — Blocs & frontend.