Chapitre 6.4 — Tutoriel complet : un bloc (block_progressinfo)
Partie 6 : Développer des plugins — Chapitre 4/7 Public : développeur ayant fait le chapitre 6.3 (
local_todolist). On réutilise le même socle (version, capabilities, DML, Mustache) mais sous le contrat du typeblock. Version de référence : Moodle 5.2 (PHP 8.3+, racine webpublic/).
⏱️ TL;DR
- Un bloc est une boîte latérale (tiroir droit, tableau de bord). Contrat minimal : une classe
block_xxx extends block_basedansblock_xxx.php,version.php,lang/, et deux capabilities:addinstance/:myaddinstance.- La méthode centrale est
get_content(): elle retourne unstdClassavec->text(et->footer). C’est le seul « rendu » obligatoire.applicable_formats()décide où le bloc peut être ajouté (page de cours, tableau de bord, site…).instance_allow_multiple()autorise plusieurs instances.- La configuration par instance passe par
edit_form.php(block_edit_form) +specialization(): chaque instance stocke sa config dans$this->config.- Deux capabilities distinctes :
:addinstance(ajouter le bloc sur une page normale, ex. cours) et:myaddinstance(l’ajouter sur son tableau de bord/my).- On calcule la progression avec l’API de complétion :
\core_completion\progress::get_course_progress_percentage($course, $userid).- On garde la logique de rendu propre :
get_content()délègue à un template Mustache viarender_from_template, et un module AMD léger rafraîchit la barre sans recharger la page.- On peut imposer le bloc au tableau de bord par défaut de tous les utilisateurs (page « Default Dashboard » de l’administration).
🎯 Objectifs
À la fin de ce chapitre, vous saurez :
- Écrire un plugin bloc fonctionnel et le placer sur une page de cours et sur le tableau de bord.
- Maîtriser
get_content(),applicable_formats(),instance_allow_multiple(),specialization(). - Ajouter une configuration par instance avec
edit_form.php. - Comprendre les capabilities
:addinstancevs:myaddinstance. - Greffer un module AMD léger et imposer un bloc au tableau de bord par défaut.
1. Ce qu’est un bloc, et quand en écrire un
Un bloc (block_*) est un composant d’interface contextuel et repositionnable : les petites boîtes du tiroir droit d’un cours (Calendrier, Événements à venir…) ou du tableau de bord (Chronologie). Rappel du chapitre 6.1 : on choisit un bloc quand on veut afficher une information ou un raccourci en marge d’une page existante, sans créer une page à part entière ni une activité de cours.
Notre objectif : block_progressinfo, un bloc qui affiche la progression de complétion de l’utilisateur — sur une page de cours, le pourcentage d’achèvement de ce cours ; c’est visuel (une barre) et cliquable pour rafraîchir.
💡 Pour un dev React : un bloc, c’est un widget que l’utilisateur (enseignant ou lui-même) glisse dans une zone (une region) d’une page. Chaque placement est une instance avec sa propre config — comme un composant paramétrable qu’on drag-and-drop dans un CMS (pensez à un « widget » WordPress/Gutenberg, ou aux blocs d’un page-builder). Le plugin définit le composant ; l’instance est le composant monté à un endroit, avec ses props.
Arborescence :
public/blocks/progressinfo/
├── version.php
├── block_progressinfo.php ← LA classe (contrat du type block)
├── edit_form.php ← config par instance
├── db/
│ └── access.php
├── classes/
│ └── output/
│ ├── progress.php ← renderable
│ └── renderer.php
├── templates/
│ └── content.mustache
├── amd/
│ ├── src/
│ │ └── refresh.js ← module source (ES6)
│ └── build/ ← généré par grunt (non versionné en dev)
├── lang/
│ ├── en/block_progressinfo.php
│ └── fr/block_progressinfo.php
└── styles.css⚠️ Piège de nommage (spécifique aux blocs) : le fichier de la classe doit s’appeler
block_<nom>.phpet contenir la classeblock_<nom>— iciblock_progressinfo.php/class block_progressinfo. Contrairement aux autres types, ce n’est pas dansclasses/(autoloadé) mais à la racine du plugin, car le core le charge par convention de chemin. Le dossier, lui, estblocks/progressinfo/(plurielblocks, sans le préfixeblock_).
2. version.php et les capabilities
Identique en esprit au chapitre précédent — seul le component change.
<?php
// public/blocks/progressinfo/version.php
defined('MOODLE_INTERNAL') || die();
$plugin->component = 'block_progressinfo'; // type « block » + nom « progressinfo »
$plugin->version = 2026070800;
$plugin->requires = 2026042000; // Moodle 5.2
$plugin->maturity = MATURITY_STABLE;
$plugin->release = 'v1.0';Les blocs ont deux capabilities standard que Moodle attend :
<?php
// public/blocks/progressinfo/db/access.php
defined('MOODLE_INTERNAL') || die();
$capabilities = [
// Ajouter le bloc sur une page « normale » (cours, etc.).
'block/progressinfo:addinstance' => [
'riskbitmask' => RISK_SPAM,
'captype' => 'write',
'contextlevel' => CONTEXT_BLOCK,
'archetypes' => [
'editingteacher' => CAP_ALLOW,
'manager' => CAP_ALLOW,
],
'clonepermissionsfrom' => 'moodle/site:manageblocks',
],
// Ajouter le bloc sur SON tableau de bord (/my).
'block/progressinfo:myaddinstance' => [
'captype' => 'write',
'contextlevel' => CONTEXT_SYSTEM,
'archetypes' => [
'user' => CAP_ALLOW, // tout utilisateur connecté peut l'ajouter à son dashboard.
],
'clonepermissionsfrom' => 'moodle/my:manageblocks',
],
];La distinction est fondamentale et propre aux blocs :
| Capability | Autorise à… | Accordée par défaut à |
|---|---|---|
:addinstance | Ajouter le bloc sur une page partagée (cours, catégorie, site) | enseignant éditeur, manager |
:myaddinstance | Ajouter le bloc sur son propre tableau de bord /my | tout utilisateur (user) |
Sans :myaddinstance, votre bloc n’apparaîtra pas dans le sélecteur « Ajouter un bloc » du tableau de bord, même s’il marche parfaitement dans un cours. Beaucoup de débutants oublient cette seconde capability et se demandent pourquoi le bloc est « invisible » sur le dashboard.
⚠️ Piège :
clonepermissionsfromest une commodité — à l’installation, Moodle copie les autorisations d’une capability existante (moodle/site:manageblocks) pour votre nouvelle capability, ce qui évite de reconfigurer tous les rôles. Utile, mais ne dispense pas de déclarer lesarchetypes. Et rappel : toute modif d’access.php⇒ bump deversion.phppour être prise en compte.
3. La classe du bloc : le cœur du contrat
Voici block_progressinfo.php, commenté méthode par méthode.
<?php
// public/blocks/progressinfo/block_progressinfo.php
defined('MOODLE_INTERNAL') || die();
class block_progressinfo extends block_base {
/**
* Initialisation : appelée à chaque instanciation. On y fixe le titre par défaut.
*/
public function init(): void {
$this->title = get_string('pluginname', 'block_progressinfo');
}
/**
* Applique la config d'instance APRÈS chargement. Permet ici de surcharger le titre.
*/
public function specialization(): void {
if (isset($this->config->title) && trim($this->config->title) !== '') {
$this->title = format_string($this->config->title);
}
}
/**
* Où ce bloc peut-il être ajouté ?
* Clés : 'course-view' (page de cours), 'my' (tableau de bord),
* 'site' (page d'accueil), 'all' (partout). Valeur bool.
*/
public function applicable_formats(): array {
return [
'course-view' => true, // pages de cours
'my' => true, // tableau de bord
'site' => false, // pas sur la page d'accueil
];
}
/**
* Autorise-t-on plusieurs instances du bloc sur la même page ?
* Non : une barre de progression par page suffit.
*/
public function instance_allow_multiple(): bool {
return false;
}
/**
* Le bloc a-t-il une configuration GLOBALE (settings.php) ? Ici non.
*/
public function has_config(): bool {
return false;
}
/**
* LE cœur du bloc : produit son contenu.
*
* @return stdClass { text, footer }
*/
public function get_content(): stdClass {
global $USER, $COURSE, $PAGE;
// Mise en cache interne : get_content peut être appelée plusieurs fois par requête.
if ($this->content !== null) {
return $this->content;
}
$this->content = new stdClass();
$this->content->footer = '';
// Un visiteur non connecté n'a pas de progression.
if (!isloggedin() || isguestuser()) {
$this->content->text = get_string('notavailable', 'block_progressinfo');
return $this->content;
}
// Sur le tableau de bord, il n'y a pas de « cours courant » : message adapté.
if ($COURSE->id == SITEID) {
$this->content->text = get_string('opencourse', 'block_progressinfo');
return $this->content;
}
// Calcul de la progression de complétion du cours pour cet utilisateur.
$percentage = \core_completion\progress::get_course_progress_percentage($COURSE, $USER->id);
if ($percentage === null) {
// La complétion n'est pas activée sur ce cours.
$this->content->text = get_string('completiondisabled', 'block_progressinfo');
return $this->content;
}
$percentage = (int) round($percentage);
// Rendu via l'Output API (renderable + template), pas de HTML en dur.
$renderer = $PAGE->get_renderer('block_progressinfo');
$renderable = new \block_progressinfo\output\progress(
$percentage,
$COURSE->id,
$this->instance->id
);
$this->content->text = $renderer->render($renderable);
return $this->content;
}
}Les méthodes à connaître, par ordre d’importance :
get_content()— obligatoire, c’est le rendu. Elle retourne unstdClassavec->text(corps) et->footer(bas du bloc, optionnel). Le cache interneif ($this->content !== null)est un idiome standard : le core peut appelerget_content()plusieurs fois (une fois pour savoir si le bloc est vide, une fois pour l’afficher). Un bloc dont->textest vide n’est pas rendu.init()— appelée à chaque construction ; on y met le titre par défaut et rien de coûteux (pas de requête).applicable_formats()— la whitelist des emplacements. Renvoyer'my' => trueest ce qui, combiné à:myaddinstance, rend le bloc ajoutable au tableau de bord.specialization()— appelée après que$this->config(la config d’instance) est chargée ; on y applique cette config (ici surcharger le titre). À ne pas confondre avecinit().instance_allow_multiple()— plusieurs instances sur une même page, oui/non.has_config()— existence d’une config globale (au niveau site, viasettings.php), distincte de la config par instance (edit_form.php).
💡 Pour un dev React :
get_content()est votre fonction de rendu (render()),$this->configsont les props d’instance (la config que l’utilisateur a réglée pour CE placement),$this->instanceporte l’identité (id, contexte, position).specialization()≈ uncomponentDidMount/useEffectqui applique les props après hydratation. Le cache$this->contentévite un double rendu, comme unuseMemo.
4. La configuration par instance : edit_form.php
On veut permettre à celui qui pose le bloc de surcharger son titre. La config par instance passe par un formulaire dédié, edit_form.php, qui étend block_edit_form (une variante de moodleform propre aux blocs).
<?php
// public/blocks/progressinfo/edit_form.php
defined('MOODLE_INTERNAL') || die();
class block_progressinfo_edit_form extends block_edit_form {
protected function specific_definition($mform): void {
// Section de config propre au bloc.
$mform->addElement('header', 'configheader',
get_string('blocksettings', 'block'));
// Champ « titre personnalisé ».
$mform->addElement('text', 'config_title',
get_string('configtitle', 'block_progressinfo'));
$mform->setType('config_title', PARAM_TEXT);
$mform->setDefault('config_title', '');
// Choix d'affichage : barre ou pourcentage seul.
$mform->addElement('selectyesno', 'config_showbar',
get_string('configshowbar', 'block_progressinfo'));
$mform->setDefault('config_showbar', 1);
}
}Trois règles spécifiques aux blocs :
- On surcharge
specific_definition($mform), pasdefinition(). Le core ajoute déjà autour les réglages communs (visibilité, position, régions). - Les champs de config d’instance sont préfixés par
config_:config_title,config_showbar. Ce préfixe est la convention qui relie le champ du formulaire à$this->config->title/$this->config->showbardans la classe. Le préfixe est obligatoire, sinon la valeur n’est pas persistée dans la config d’instance. - Aucune écriture en base à faire : Moodle sérialise automatiquement la config d’instance (dans la table
mdl_block_instances, colonneconfigdata). Vous lisez ensuite via$this->config->….
Pour exploiter config_showbar, on le passe au renderable (voir §5) ; la classe du §3 le lirait via $this->config->showbar.
⚠️ Piège : la config d’instance est par placement, pas globale. Deux instances du même bloc (si
instance_allow_multiple()renvoyaittrue) ont desconfigdataindépendants. Ne stockez jamais de config d’instance dans une table à vous ni dansset_config()global — laissez Moodle gérerconfigdata.
5. Le rendu : renderable + renderer + Mustache
Comme au chapitre précédent, on sort le HTML de la logique. Le renderable prépare le pourcentage et les options ; le template dessine la barre Bootstrap.
<?php
// public/blocks/progressinfo/classes/output/progress.php
namespace block_progressinfo\output;
use renderable;
use templatable;
use renderer_base;
use moodle_url;
defined('MOODLE_INTERNAL') || die();
class progress implements renderable, templatable {
public function __construct(
private int $percentage,
private int $courseid,
private int $instanceid
) {}
public function export_for_template(renderer_base $output): array {
return [
'percentage' => $this->percentage,
'complete' => $this->percentage >= 100,
'courseid' => $this->courseid,
'instanceid' => $this->instanceid,
// couleur de la barre selon l'avancement.
'barclass' => $this->percentage >= 100 ? 'bg-success'
: ($this->percentage >= 50 ? 'bg-info' : 'bg-warning'),
];
}
}<?php
// public/blocks/progressinfo/classes/output/renderer.php
namespace block_progressinfo\output;
defined('MOODLE_INTERNAL') || die();
class renderer extends \plugin_renderer_base {
public function render_progress(progress $renderable): string {
$context = $renderable->export_for_template($this);
return $this->render_from_template('block_progressinfo/content', $context);
}
}{{!
public/blocks/progressinfo/templates/content.mustache
Contexte : percentage int, complete bool, barclass, courseid, instanceid
}}
<div class="block-progressinfo" data-region="progressinfo" data-instanceid="{{instanceid}}" data-courseid="{{courseid}}">
<div class="progress" style="height: 1.25rem;" role="progressbar"
aria-valuenow="{{percentage}}" aria-valuemin="0" aria-valuemax="100">
<div class="progress-bar {{barclass}}" style="width: {{percentage}}%;">
{{percentage}}%
</div>
</div>
{{#complete}}
<p class="mt-2 mb-0 text-success">{{#str}}allcomplete, block_progressinfo{{/str}}</p>
{{/complete}}
<button type="button" class="btn btn-sm btn-link p-0 mt-2" data-action="refresh">
{{#str}}refresh, block_progressinfo{{/str}}
</button>
</div>Notez les data-* (data-region, data-instanceid, data-courseid, data-action) : c’est le point d’accroche du JavaScript (§6). Le rendu serveur pose des marqueurs ; le JS les cible sans jamais deviner de structure DOM fragile.
⚠️ Piège d’accessibilité : une barre de progression doit porter
role="progressbar"et les attributsaria-valuenow/min/max. Le lecteur d’écran annonce alors « 60 % ». Un simple<div>coloré serait invisible pour l’accessibilité — sujet approfondi en Partie 8.
6. Un module AMD léger pour rafraîchir
Ajoutons un bouton « Rafraîchir » qui recalcule la progression sans recharger la page. C’est l’occasion de voir le pont JS ↔ serveur (approfondi en Partie 7). Le module est volontairement minimal.
// public/blocks/progressinfo/amd/src/refresh.js
import Ajax from 'core/ajax';
import Templates from 'core/templates';
import Notification from 'core/notification';
/**
* Initialise le bouton de rafraîchissement d'une instance de bloc.
*
* @param {number} instanceId l'id de l'instance de bloc
*/
export const init = (instanceId) => {
const root = document.querySelector(
`.block-progressinfo[data-instanceid="${instanceId}"]`
);
if (!root) {
return;
}
const button = root.querySelector('[data-action="refresh"]');
button?.addEventListener('click', async () => {
const courseId = parseInt(root.dataset.courseid, 10);
try {
// Appel d'une fonction de web service (déclarée en db/services.php — voir Partie 9).
const response = await Ajax.call([{
methodname: 'block_progressinfo_get_progress',
args: {courseid: courseId},
}])[0];
// Re-render du contenu via le même template Mustache que le serveur.
const {html, js} = await Templates.renderForPromise(
'block_progressinfo/content',
response
);
Templates.replaceNodeContents(root.parentNode, html, js);
} catch (error) {
Notification.exception(error);
}
});
};Points clés (le détail est en Partie 7 et 9) :
- Un module AMD exporte une fonction
initappelée depuis le PHP via$PAGE->requires->js_call_amd('block_progressinfo/refresh', 'init', [$instanceid])— à ajouter dansget_content()avant de retourner. core/ajaxappelle une fonction de web service (block_progressinfo_get_progress), qu’on déclarerait endb/services.phpet qu’on écrirait comme external function (Partie 9). Le JS ne parle jamais SQL : il passe par une API serveur contrôlée (sécurité, validation des paramètres).core/templatesrend le même template Mustache côté client : la cohérence serveur/client est garantie, on n’a pas deux versions du HTML.- Écrit en ES6 dans
amd/src/, transpilé enamd/build/par Grunt (grunt amd). En dev,$CFG->cachejs = false;évite d’avoir à rebuilder à chaque essai.
💡 Pour un dev React :
core/ajax+core/templatesforment un mini « fetch + render » : vous appelez une API typée (l’external function joue le rôle de route handler côté serveur) et vous re-render un composant Mustache. C’est un îlot de JS greffé sur du rendu serveur — la philosophie « server-first + hydratation ciblée » détaillée en Partie 7. Ici, volontairement, pas de framework : un bloc n’en a pas besoin.
⚠️ Piège : ne mettez pas de logique métier (calcul de progression, accès données) dans le JS. Le JS demande au serveur et affiche. Toute la vérité (permissions, calcul, données) reste serveur. Un JS qui calculerait lui-même la progression serait à la fois faux (il n’a pas les données) et contournable.
7. Installation et placement
- Déposez
public/blocks/progressinfo/avec tous les fichiers. - Connecté en admin, visitez une page : Moodle détecte le plugin, propose l’upgrade (enregistre les capabilities). En CLI :
php admin/cli/upgrade.php, puisgrunt amdpour compiler le JS, puisphp admin/cli/purge_caches.php. - Sur un cours (avec complétion activée) : mode édition → tiroir droit → « Ajouter un bloc » → « My progress ». La barre s’affiche. → nécessite
:addinstance(l’enseignant éditeur l’a). - Sur le tableau de bord (
/my) : mode édition → « Ajouter un bloc » → « My progress ». → nécessite:myaddinstance(tout utilisateur l’a). Sur le dashboard, notre bloc affichera le message « ouvrez un cours » (pas de cours courant) — on pourrait l’enrichir pour agréger tous les cours (exercice 4).
8. Imposer le bloc au tableau de bord par défaut
Souvent on veut que tous les utilisateurs aient le bloc sur leur tableau de bord, sans qu’ils l’ajoutent eux-mêmes. Deux approches.
Approche interface (recommandée pour commencer) : Administration du site → Apparence → Tableau de bord par défaut (Default Dashboard page). Cliquez « Modifier ce tableau de bord », ajoutez votre bloc, puis « Réinitialiser les tableaux de bord de tous les utilisateurs ». Les utilisateurs sans personnalisation héritent du nouveau bloc.
Approche « bloc collant » (sticky block) : sur n’importe quel bloc, en mode édition, la config d’instance permet de le rendre visible sur toutes les pages d’un certain type et d’empêcher son déplacement/suppression (« Bloquer partout »). Pour un déploiement contrôlé, on privilégie toutefois le tableau de bord par défaut.
⚠️ Piège : « Réinitialiser les tableaux de bord » écrase les personnalisations des utilisateurs qui avaient déjà arrangé le leur. Communiquez avant de le faire en production, ou réservez-le à un premier déploiement.
📚 Aller plus loin : la mécanique des blocs par défaut et des régions est documentée sur moodledev.io/docs/5.2/apis/plugintypes/blocks . Le code de
block_recentlyaccesseditems(livré avec Moodle, présent par défaut sur le dashboard) est une excellente lecture de référence : bloc moderne, template Mustache, AMD.
9. Récapitulatif : bloc vs plugin local
Vous venez de réutiliser tout le socle du chapitre 6.3 (version, capabilities, Output API, Mustache, AMD) sous un contrat différent. Comparons :
| Aspect | local_todolist (ch. 6.3) | block_progressinfo (ce ch.) |
|---|---|---|
| Point d’entrée | Pages index.php / edit.php | Classe block_* + get_content() |
| Où ça vit | Une page à soi | Une boîte dans une région d’une autre page |
| Rendu | $OUTPUT->header() + renderer | get_content()->text (pas de header/footer de page) |
| Placement | URL fixe | Instanciable par l’utilisateur (drag & drop) |
| Config | Réglages globaux (settings.php) | Config par instance (edit_form.php) + éventuellement globale |
| Capabilities | une, pour l’utilisateur | deux : :addinstance, :myaddinstance |
| Emplacements | (page dédiée) | contrôlés par applicable_formats() |
C’est la leçon de fond de la Partie 6 : le socle est commun, le contrat de fichiers change. Une fois local et block compris, le type le plus riche — le module d’activité — n’est « que » un contrat plus vaste par-dessus le même fond. C’est l’objet du chapitre suivant.
✏️ Exercices
Exercice 1 — Le bloc n’apparaît pas sur le dashboard.
Votre bloc fonctionne dans les cours mais est absent du sélecteur « Ajouter un bloc » du tableau de bord. applicable_formats() renvoie bien 'my' => true. Quelle est la cause la plus probable ?
✅ Solution
Il manque (ou n’est pas accordée) la capability block/progressinfo:myaddinstance. applicable_formats() dit que le bloc peut aller sur /my, mais c’est :myaddinstance qui autorise l’utilisateur à l’y ajouter. Vérifiez sa présence dans db/access.php (avec archetypes => ['user' => CAP_ALLOW]), puis bumpez version.php et rejouez l’upgrade (sinon la capability n’est pas enregistrée), et purgez les caches. Rappel : :addinstance (cours) et :myaddinstance (dashboard) sont deux capabilities distinctes.
Exercice 2 — Titre personnalisé non pris en compte.
Vous avez ajouté config_title dans edit_form.php, mais le titre saisi ne s’affiche jamais. Le champ est bien enregistré. Où est l’oubli ?
✅ Solution
Probablement l’absence (ou l’erreur) de specialization() dans la classe. C’est cette méthode, appelée après le chargement de $this->config, qui doit surcharger $this->title à partir de $this->config->title. Vérifiez aussi le préfixe config_ dans le formulaire (config_title) : sans lui, la valeur n’est pas sérialisée dans configdata et $this->config->title reste vide. Enfin, format_string() sur le titre pour la sortie sûre.
Exercice 3 — get_content() appelée deux fois.
Pourquoi voit-on l’idiome if ($this->content !== null) return $this->content; au début de get_content() ? Que se passerait-il sans lui si le calcul était coûteux ?
✅ Solution
Le core appelle get_content() plusieurs fois par requête : au moins une fois pour déterminer si le bloc est vide (auquel cas il n’est pas rendu) et une fois pour l’afficher. L’idiome met en cache l’objet $this->content : sans lui, un calcul coûteux (ici, la progression de complétion, qui interroge la base) serait exécuté deux fois par affichage — gaspillage pur. C’est un pattern obligatoire pour tout bloc non trivial.
Exercice 4 — Agréger la progression sur le dashboard. Sur le tableau de bord, notre bloc affiche « ouvrez un cours ». On veut plutôt afficher la progression moyenne sur tous les cours de l’utilisateur. Décrivez l’approche (sans tout coder).
✅ Solution
Dans get_content(), quand $COURSE->id == SITEID, récupérer la liste des cours de l’utilisateur via enrol_get_my_courses(), puis pour chacun appeler \core_completion\progress::get_course_progress_percentage($course, $USER->id). Ignorer les cours où la complétion est désactivée (null), moyenner les pourcentages restants, et passer cette moyenne (plus, idéalement, la liste détaillée) à un renderable/template adapté. Attention à la performance : boucler sur beaucoup de cours multiplie les requêtes (risque N+1) — mettre en cache le résultat (MUC, Partie 5) ou limiter aux cours « en cours ». C’est un bon exemple où l’utilité fonctionnelle se paie en vigilance perf.
Exercice 5 — Sécuriser le web service de rafraîchissement.
Le module AMD appelle block_progressinfo_get_progress(courseid). Quels contrôles l’external function correspondante doit-elle impérativement faire côté serveur ?
✅ Solution
Au minimum : (1) require_login($course) / validation que l’utilisateur est bien inscrit et peut voir le cours ; (2) validation du contexte du cours et vérification des capabilities pertinentes ; (3) validation des paramètres via la déclaration de l’external function (courseid en PARAM_INT, structure d’execute_parameters) ; (4) ne renvoyer que la progression de l’utilisateur courant ($USER->id), jamais celle d’un autre passé en paramètre. Le JS est non fiable : tout paramètre venant du client est potentiellement falsifié. Toute la sécurité vit dans l’external function — détaillé en Partie 9. C’est l’application directe de « autorisation ≠ appartenance » du chapitre 6.3, côté web service.
🧠 Quiz de révision
Q1. Où doit se trouver la classe d’un bloc et comment doit-elle s’appeler ?
Réponse
classes/), dans un fichier block_<nom>.php contenant class block_<nom> extends block_base. Ex. blocks/progressinfo/block_progressinfo.php → class block_progressinfo. Le core la charge par convention de chemin.Q2. Quelle est la différence entre :addinstance et :myaddinstance ?
Réponse
:addinstance autorise à ajouter le bloc sur une page partagée (cours, catégorie, site) ; :myaddinstance autorise à l’ajouter sur son propre tableau de bord /my. Ce sont deux capabilities distinctes ; oublier la seconde rend le bloc invisible dans le sélecteur du dashboard.Q3. Que retourne get_content() et pourquoi le cache interne $this->content ?
Réponse
stdClass avec ->text (le corps du bloc) et ->footer (optionnel). Le cache if ($this->content !== null) return $this->content; évite de recalculer : le core appelle get_content() plusieurs fois par requête (test de vacuité + affichage). Un bloc dont ->text est vide n’est pas rendu.Q4. À quoi servent applicable_formats() et specialization() ?
Réponse
applicable_formats() est la whitelist des emplacements où le bloc peut être ajouté (course-view, my, site, all…). specialization(), appelée après le chargement de $this->config, applique la config d’instance (par ex. surcharger le titre) — à distinguer d’init(), qui ne fixe que les valeurs par défaut sans accès à la config.Q5. Où est stockée la configuration par instance d’un bloc, et quel préfixe portent ses champs de formulaire ?
Réponse
configdata de la table mdl_block_instances (une entrée par instance/placement). Les champs du edit_form.php doivent être préfixés par config_ (config_title, config_showbar…) : ce préfixe est la convention qui relie le champ à $this->config->title / ->showbar. On ne crée jamais de table maison pour ça.Chapitre suivant : 05 — Tutoriel complet : un module d’activité (mod_simplecheck) — le plus gros tutoriel de la documentation. On attaque le type le plus riche : mod_form.php, view.php, les fonctions _add_instance/_update_instance/_delete_instance, les FEATURE_*, l’intégration au carnet de notes, l’achèvement d’activité, les events, le backup/restore et le reset de cours.