Skip to Content
MoodlePartie 6 — Développer des pluginsTutoriel complet : un bloc (`block_progressinfo`)

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 type block. Version de référence : Moodle 5.2 (PHP 8.3+, racine web public/).

⏱️ TL;DR

  • Un bloc est une boîte latérale (tiroir droit, tableau de bord). Contrat minimal : une classe block_xxx extends block_base dans block_xxx.php, version.php, lang/, et deux capabilities :addinstance / :myaddinstance.
  • La méthode centrale est get_content() : elle retourne un stdClass avec ->text (et ->footer). C’est le seul « rendu » obligatoire.
  • applicable_formats() décide 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 via render_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 :addinstance vs :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>.php et contenir la classe block_<nom> — ici block_progressinfo.php / class block_progressinfo. Contrairement aux autres types, ce n’est pas dans classes/ (autoloadé) mais à la racine du plugin, car le core le charge par convention de chemin. Le dossier, lui, est blocks/progressinfo/ (pluriel blocks, sans le préfixe block_).


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 :

CapabilityAutorise à…Accordée par défaut à
:addinstanceAjouter le bloc sur une page partagée (cours, catégorie, site)enseignant éditeur, manager
:myaddinstanceAjouter le bloc sur son propre tableau de bord /mytout 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 : clonepermissionsfrom est 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 les archetypes. Et rappel : toute modif d’access.phpbump de version.php pour ê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 un stdClass avec ->text (corps) et ->footer (bas du bloc, optionnel). Le cache interne if ($this->content !== null) est un idiome standard : le core peut appeler get_content() plusieurs fois (une fois pour savoir si le bloc est vide, une fois pour l’afficher). Un bloc dont ->text est 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' => true est 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 avec init().
  • instance_allow_multiple() — plusieurs instances sur une même page, oui/non.
  • has_config() — existence d’une config globale (au niveau site, via settings.php), distincte de la config par instance (edit_form.php).

💡 Pour un dev React : get_content() est votre fonction de rendu (render()), $this->config sont les props d’instance (la config que l’utilisateur a réglée pour CE placement), $this->instance porte l’identité (id, contexte, position). specialization() ≈ un componentDidMount/useEffect qui applique les props après hydratation. Le cache $this->content évite un double rendu, comme un useMemo.


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), pas definition(). 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->showbar dans 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, colonne configdata). 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() renvoyait true) ont des configdata indépendants. Ne stockez jamais de config d’instance dans une table à vous ni dans set_config() global — laissez Moodle gérer configdata.


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 attributs aria-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 init appelée depuis le PHP via $PAGE->requires->js_call_amd('block_progressinfo/refresh', 'init', [$instanceid]) — à ajouter dans get_content() avant de retourner.
  • core/ajax appelle une fonction de web service (block_progressinfo_get_progress), qu’on déclarerait en db/services.php et 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/templates rend 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é en amd/build/ par Grunt (grunt amd). En dev, $CFG->cachejs = false; évite d’avoir à rebuilder à chaque essai.

💡 Pour un dev React : core/ajax + core/templates forment 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

  1. Déposez public/blocks/progressinfo/ avec tous les fichiers.
  2. Connecté en admin, visitez une page : Moodle détecte le plugin, propose l’upgrade (enregistre les capabilities). En CLI : php admin/cli/upgrade.php, puis grunt amd pour compiler le JS, puis php admin/cli/purge_caches.php.
  3. 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).
  4. 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 :

Aspectlocal_todolist (ch. 6.3)block_progressinfo (ce ch.)
Point d’entréePages index.php / edit.phpClasse block_* + get_content()
Où ça vitUne page à soiUne boîte dans une région d’une autre page
Rendu$OUTPUT->header() + rendererget_content()->text (pas de header/footer de page)
PlacementURL fixeInstanciable par l’utilisateur (drag & drop)
ConfigRéglages globaux (settings.php)Config par instance (edit_form.php) + éventuellement globale
Capabilitiesune, pour l’utilisateurdeux : :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

À la racine du plugin (pas dans classes/), dans un fichier block_<nom>.php contenant class block_<nom> extends block_base. Ex. blocks/progressinfo/block_progressinfo.phpclass 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

Elle retourne un 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

Elle est sérialisée automatiquement par Moodle dans la colonne 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.